Skip to main content

beancount_parser_lima/
parsers.rs

1use crate::{
2    lexer::Token,
3    options::{BeancountOption, BeancountOptionError, ParserOptions, DEFAULT_LONG_STRING_MAXLINES},
4    types::*,
5};
6use chumsky::{
7    input::BorrowInput,
8    prelude::{
9        any_ref, choice, end, extra, group, just, recursive, select_ref, skip_then_retry_until,
10        IterParser, Parser, Rich,
11    },
12};
13use either::Either;
14use rust_decimal::Decimal;
15use std::{
16    collections::{hash_map, HashMap, HashSet},
17    iter::once,
18    ops::Deref,
19    path::Path,
20};
21use time::Date;
22
23/// Matches all the includes in the file, ignoring everything else.
24pub(crate) fn includes<'s, I>() -> impl Parser<'s, I, Vec<String>, Extra<'s>>
25where
26    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
27{
28    (just(Token::Include).ignore_then(string()).map(Some))
29        .or(any_ref().map(|_| None))
30        .repeated()
31        .collect::<Vec<_>>()
32        .map(|includes| {
33            includes
34                .into_iter()
35                .filter_map(|s| s.as_ref().map(|s| s.to_string()))
36                .collect::<Vec<_>>()
37        })
38}
39
40/// Matches the whole file.
41pub(crate) fn file<'s, I>(
42    source_path: Option<&'s Path>,
43) -> impl Parser<'s, I, Vec<Spanned<Declaration<'s>>>, Extra<'s>>
44where
45    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
46{
47    declaration(source_path).repeated().collect::<Vec<_>>()
48}
49
50/// Matches a [Declaration], and returns with Span.
51pub(crate) fn declaration<'s, I>(
52    source_path: Option<&'s Path>,
53) -> impl Parser<'s, I, Spanned<Declaration<'s>>, Extra<'s>>
54where
55    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
56{
57    use Declaration::*;
58
59    choice((directive().map(Directive), pragma(source_path).map(Pragma)))
60        .map_with(spanned_extra)
61        .recover_with(skip_then_retry_until(any_ref().ignored(), end()))
62}
63
64/// Matches a [Directive].
65pub(crate) fn directive<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
66where
67    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
68{
69    choice((
70        transaction().labelled("transaction").as_context(),
71        choice((
72            price(),
73            balance(),
74            open(),
75            close(),
76            commodity(),
77            pad(),
78            document(),
79            note(),
80            event(),
81            query(),
82            custom(),
83        ))
84        .labelled("directive")
85        .as_context(),
86    ))
87}
88
89/// Matches a [Pragma].
90pub(crate) fn pragma<'s, I>(
91    source_path: Option<&'s Path>,
92) -> impl Parser<'s, I, Pragma<'s>, Extra<'s>>
93where
94    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
95{
96    choice((
97        just(Token::Pushtag)
98            .ignore_then(tag())
99            .map_with(|tag, e| Pragma::Pushtag(spanned(tag, e.span()))),
100        just(Token::Poptag)
101            .ignore_then(tag())
102            .map_with(|tag, e| Pragma::Poptag(spanned(tag, e.span()))),
103        just(Token::Pushmeta)
104            .ignore_then(meta_key_value())
105            .map(Pragma::Pushmeta),
106        just(Token::Popmeta)
107            .ignore_then(key())
108            .then_ignore(just(Token::Colon))
109            .map_with(|key, e| Pragma::Popmeta(spanned(key, e.span()))),
110        just(Token::Include)
111            .ignore_then(string().map_with(|path, e| Pragma::Include(spanned(path, e.span())))),
112        option(source_path).map(Pragma::Option),
113        just(Token::Plugin)
114            .ignore_then(string().map_with(spanned_extra))
115            .then(string().map_with(spanned_extra).or_not())
116            .map(|(module_name, config)| {
117                Pragma::Plugin(Plugin {
118                    module_name,
119                    config,
120                })
121            }),
122    ))
123    .then_ignore(just(Token::Eol))
124    .labelled("directive") // yeah, pragma is not a user-facing concept
125    .as_context()
126}
127
128/// Matches a [BeancountOption], failing if the option cannot be processed.
129pub(crate) fn option<'s, I>(
130    source_path: Option<&'s Path>,
131) -> impl Parser<'s, I, BeancountOption<'s>, Extra<'s>>
132where
133    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
134{
135    just(Token::Option)
136        .ignore_then(string().map_with(|name, e| spanned(name, e.span())))
137        .then(string().map_with(|value, e| spanned(value, e.span())))
138        .validate(move |(name, value), e, emitter| {
139            // validate allows us to emit an error for a bad option but still consume the input,
140            // but we have to return the dummy Ignored option
141
142            use BeancountOptionError::*;
143
144            let opt = BeancountOption::parse(name, value, source_path)
145                .map_err(|e| match e {
146                    UnknownOption => Rich::custom(name.span, e.to_string()),
147                    BadValue(_) => Rich::custom(value.span, e.to_string()),
148                })
149                .and_then(|opt| {
150                    let parser_state: &mut extra::SimpleState<ParserState> = e.state();
151                    parser_state
152                        .options
153                        .assimilate(opt)
154                        .map_err(|e| Rich::custom(value.span, e.to_string()))
155                });
156
157            opt.unwrap_or_else(|e| {
158                emitter.emit(e);
159                BeancountOption::ignored()
160            })
161        })
162}
163
164/// Matches a transaction, including metadata and postings, over several lines.
165pub(crate) fn transaction<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
166where
167    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
168{
169    group((
170        transaction_header_line(),
171        metadata().map_with(spanned_extra),
172        posting().repeated().collect::<Vec<_>>(),
173    ))
174    .validate(
175        |((date, flag, (payee, narration), (tags, links)), mut metadata, postings),
176         _span,
177         emitter| {
178            metadata.merge_tags(&tags, emitter);
179            metadata.merge_links(&links, emitter);
180
181            Directive {
182                date,
183                metadata,
184                variant: DirectiveVariant::Transaction(Transaction {
185                    flag,
186                    payee,
187                    narration,
188                    postings,
189                }),
190            }
191        },
192    )
193}
194
195type TransactionHeaderLine<'s> = (
196    Spanned<Date>,
197    Spanned<Flag>,
198    (Option<Spanned<&'s str>>, Option<Spanned<&'s str>>),
199    (HashSet<Spanned<Tag<'s>>>, HashSet<Spanned<Link<'s>>>),
200);
201
202/// Matches the first line of a transaction.
203fn transaction_header_line<'s, I>() -> impl Parser<'s, I, TransactionHeaderLine<'s>, Extra<'s>>
204where
205    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
206{
207    group((
208        date().map_with(spanned_extra),
209        txn().map_with(spanned_extra),
210        // payee and narration get special handling in case one is omitted
211        group((
212            string().map_with(spanned_extra).or_not(),
213            string().map_with(spanned_extra).or_not(),
214        ))
215        .map(|(s1, s2)| match (s1, s2) {
216            // a single string is narration
217            (Some(s1), None) => (None, Some(s1)),
218            (s1, s2) => (s1, s2),
219        })
220        .map(|(payee, narration)| {
221            (
222                replace_some_empty_with_none(payee),
223                replace_some_empty_with_none(narration),
224            )
225        }),
226        tags_links(),
227    ))
228    .then_ignore(just(Token::Eol))
229}
230
231fn replace_some_empty_with_none(s: Option<Spanned<&str>>) -> Option<Spanned<&str>> {
232    match s {
233        Some(maybe_empty) => {
234            if maybe_empty.is_empty() {
235                None
236            } else {
237                s
238            }
239        }
240        None => None,
241    }
242}
243
244/// Matches a price directive, including metadata, over several lines.
245pub(crate) fn price<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
246where
247    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
248{
249    group((
250        date().map_with(spanned_extra),
251        just(Token::Price),
252        currency().map_with(spanned_extra),
253        amount().map_with(spanned_extra),
254        tags_links(),
255    ))
256    .then_ignore(just(Token::Eol))
257    .then(metadata().map_with(spanned_extra))
258    .validate(
259        |((date, _, currency, amount, (tags, links)), mut metadata), _span, emitter| {
260            metadata.merge_tags(&tags, emitter);
261            metadata.merge_links(&links, emitter);
262            Directive {
263                date,
264                metadata,
265                variant: DirectiveVariant::Price(Price { currency, amount }),
266            }
267        },
268    )
269    .labelled("price")
270    .as_context()
271}
272
273/// Matches a balance directive, including metadata, over several lines.
274pub(crate) fn balance<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
275where
276    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
277{
278    group((
279        date().map_with(spanned_extra),
280        just(Token::Balance),
281        account().map_with(spanned_extra),
282        amount_with_tolerance().map_with(spanned_extra),
283        tags_links(),
284    ))
285    .then_ignore(just(Token::Eol))
286    .then(metadata().map_with(spanned_extra))
287    .validate(
288        |((date, _, account, atol, (tags, links)), mut metadata), _span, emitter| {
289            metadata.merge_tags(&tags, emitter);
290            metadata.merge_links(&links, emitter);
291            Directive {
292                date,
293                metadata,
294                variant: DirectiveVariant::Balance(Balance { account, atol }),
295            }
296        },
297    )
298    .labelled("balance")
299    .as_context()
300}
301
302/// Matches a open, including metadata, over several lines.
303pub(crate) fn open<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
304where
305    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
306{
307    group((open_header_line(), metadata().map_with(spanned_extra))).validate(
308        |((date, account, currencies, booking, (tags, links)), mut metadata), _span, emitter| {
309            metadata.merge_tags(&tags, emitter);
310            metadata.merge_links(&links, emitter);
311
312            Directive {
313                date,
314                metadata,
315                variant: DirectiveVariant::Open(Open {
316                    account,
317                    currencies,
318                    booking,
319                }),
320            }
321        },
322    )
323}
324
325type OpenHeaderLine<'s> = (
326    Spanned<Date>,
327    Spanned<Account<'s>>,
328    HashSet<Spanned<Currency<'s>>>,
329    Option<Spanned<Booking>>,
330    (HashSet<Spanned<Tag<'s>>>, HashSet<Spanned<Link<'s>>>),
331);
332
333/// Matches the first line of a open.
334fn open_header_line<'s, I>() -> impl Parser<'s, I, OpenHeaderLine<'s>, Extra<'s>>
335where
336    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
337{
338    group((
339        date().map_with(spanned_extra),
340        just(Token::Open),
341        account().map_with(spanned_extra),
342        currency_list(),
343        booking().map_with(spanned_extra).or_not(),
344        tags_links(),
345    ))
346    .then_ignore(just(Token::Eol))
347    .map(|(date, _, account, currency, booking, tags_links)| {
348        (date, account, currency, booking, tags_links)
349    })
350}
351
352/// Matches zero or more currencies, comma-separated.
353fn currency_list<'s, I>() -> impl Parser<'s, I, HashSet<Spanned<Currency<'s>>>, Extra<'s>>
354where
355    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
356{
357    group((
358        currency().map_with(spanned_extra),
359        (just(Token::Comma).ignore_then(currency().map_with(spanned_extra)))
360            .repeated()
361            .collect::<Vec<_>>(),
362    ))
363    .validate(|(first_currency, mut currencies), _span, emitter| {
364        currencies.push(first_currency);
365        currencies
366            .into_iter()
367            .fold(HashSet::new(), |mut currencies, currency| {
368                if currencies.contains(&currency) {
369                    emitter.emit(Rich::custom(
370                        currency.span,
371                        format!("duplicate currency {}", currency),
372                    ))
373                } else {
374                    currencies.insert(currency);
375                }
376
377                currencies
378            })
379    })
380    .or_not()
381    .map(|currencies| currencies.unwrap_or_default())
382}
383
384/// Matches a [Account].
385fn account<'s, I>() -> impl Parser<'s, I, Account<'s>, Extra<'s>>
386where
387    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
388{
389    let s = select_ref!(Token::Account(s) => *s);
390
391    s.try_map_with(|s, e| {
392        let span = e.span();
393        //
394        // look up the account type name to see which account type it is currently mapped to
395        let parser_state: &mut extra::SimpleState<ParserState> = e.state();
396        let account_type_names = &parser_state.options.account_type_names;
397
398        Account::new(s, account_type_names).map_err(|e| Rich::custom(span, e.to_string()))
399    })
400}
401
402/// Matches a [Booking].
403fn booking<'s, I>() -> impl Parser<'s, I, Booking, Extra<'s>>
404where
405    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
406{
407    string().try_map(|s, span| Booking::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
408}
409
410/// Matches a close, including metadata, over several lines.
411pub(crate) fn close<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
412where
413    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
414{
415    group((
416        date().map_with(spanned_extra),
417        just(Token::Close),
418        account().map_with(spanned_extra),
419        tags_links(),
420    ))
421    .then_ignore(just(Token::Eol))
422    .then(metadata().map_with(spanned_extra))
423    .validate(
424        |((date, _, account, (tags, links)), mut metadata), _span, emitter| {
425            metadata.merge_tags(&tags, emitter);
426            metadata.merge_links(&links, emitter);
427
428            Directive {
429                date,
430                metadata,
431                variant: DirectiveVariant::Close(Close { account }),
432            }
433        },
434    )
435}
436
437/// Matches a commodity, including metadata, over several lines.
438pub(crate) fn commodity<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
439where
440    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
441{
442    group((
443        date().map_with(spanned_extra),
444        just(Token::Commodity),
445        currency().map_with(spanned_extra),
446        tags_links(),
447    ))
448    .then_ignore(just(Token::Eol))
449    .then(metadata().map_with(spanned_extra))
450    .validate(
451        |((date, _, currency, (tags, links)), mut metadata), _span, emitter| {
452            metadata.merge_tags(&tags, emitter);
453            metadata.merge_links(&links, emitter);
454
455            Directive {
456                date,
457                metadata,
458                variant: DirectiveVariant::Commodity(Commodity { currency }),
459            }
460        },
461    )
462}
463
464/// Matches a pad, including metadata, over several lines.
465pub(crate) fn pad<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
466where
467    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
468{
469    group((
470        date().map_with(spanned_extra),
471        just(Token::Pad),
472        account().map_with(spanned_extra),
473        account().map_with(spanned_extra),
474        tags_links(),
475    ))
476    .then_ignore(just(Token::Eol))
477    .then(metadata().map_with(spanned_extra))
478    .validate(
479        |((date, _, account, source, (tags, links)), mut metadata), _span, emitter| {
480            metadata.merge_tags(&tags, emitter);
481            metadata.merge_links(&links, emitter);
482
483            Directive {
484                date,
485                metadata,
486                variant: DirectiveVariant::Pad(Pad { account, source }),
487            }
488        },
489    )
490}
491
492/// Matches a document, including metadata, over several lines.
493pub(crate) fn document<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
494where
495    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
496{
497    group((
498        date().map_with(spanned_extra),
499        just(Token::Document),
500        account().map_with(spanned_extra),
501        string().map_with(spanned_extra),
502        tags_links(),
503    ))
504    .then_ignore(just(Token::Eol))
505    .then(metadata().map_with(spanned_extra))
506    .validate(
507        |((date, _, account, path, (tags, links)), mut metadata), _span, emitter| {
508            metadata.merge_tags(&tags, emitter);
509            metadata.merge_links(&links, emitter);
510
511            Directive {
512                date,
513                metadata,
514                variant: DirectiveVariant::Document(Document { account, path }),
515            }
516        },
517    )
518}
519
520/// Matches a note, including metadata, over several lines.
521pub(crate) fn note<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
522where
523    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
524{
525    group((
526        date().map_with(spanned_extra),
527        just(Token::Note),
528        account().map_with(spanned_extra),
529        string().map_with(spanned_extra),
530        tags_links(),
531    ))
532    .then_ignore(just(Token::Eol))
533    .then(metadata().map_with(spanned_extra))
534    .validate(
535        |((date, _, account, comment, (tags, links)), mut metadata), _span, emitter| {
536            metadata.merge_tags(&tags, emitter);
537            metadata.merge_links(&links, emitter);
538
539            Directive {
540                date,
541                metadata,
542                variant: DirectiveVariant::Note(Note { account, comment }),
543            }
544        },
545    )
546}
547
548/// Matches an event, including metadata, over several lines.
549pub(crate) fn event<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
550where
551    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
552{
553    group((
554        date().map_with(spanned_extra),
555        just(Token::Event),
556        string().map_with(spanned_extra),
557        string().map_with(spanned_extra),
558        tags_links(),
559    ))
560    .then_ignore(just(Token::Eol))
561    .then(metadata().map_with(spanned_extra))
562    .validate(
563        |((date, _, event_type, description, (tags, links)), mut metadata), _span, emitter| {
564            metadata.merge_tags(&tags, emitter);
565            metadata.merge_links(&links, emitter);
566
567            Directive {
568                date,
569                metadata,
570                variant: DirectiveVariant::Event(Event {
571                    event_type,
572                    description,
573                }),
574            }
575        },
576    )
577}
578
579/// Matches a query, including metadata, over several lines.
580pub(crate) fn query<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
581where
582    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
583{
584    group((
585        date().map_with(spanned_extra),
586        just(Token::Query),
587        string().map_with(spanned_extra),
588        string().map_with(spanned_extra),
589        tags_links(),
590    ))
591    .then_ignore(just(Token::Eol))
592    .then(metadata().map_with(spanned_extra))
593    .validate(
594        |((date, _, name, content, (tags, links)), mut metadata), _span, emitter| {
595            metadata.merge_tags(&tags, emitter);
596            metadata.merge_links(&links, emitter);
597
598            Directive {
599                date,
600                metadata,
601                variant: DirectiveVariant::Query(Query { name, content }),
602            }
603        },
604    )
605}
606
607/// Matches a custom, including metadata, over several lines.
608pub(crate) fn custom<'s, I>() -> impl Parser<'s, I, Directive<'s>, Extra<'s>>
609where
610    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
611{
612    group((
613        date().map_with(spanned_extra),
614        just(Token::Custom),
615        string().map_with(spanned_extra),
616        meta_value()
617            .map_with(spanned_extra)
618            .repeated()
619            .collect::<Vec<_>>(),
620    ))
621    .then_ignore(just(Token::Eol))
622    .then(metadata().map_with(spanned_extra))
623    .map(|((date, _, type_, values), metadata)| Directive {
624        date,
625        metadata,
626        variant: DirectiveVariant::Custom(Custom { type_, values }),
627    })
628}
629
630/// Matches the `txn` keyword or a flag.
631pub(crate) fn txn<'s, I>() -> impl Parser<'s, I, Flag, Extra<'s>>
632where
633    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
634{
635    choice((just(Token::Txn).to(Flag::default()), flag()))
636}
637
638/// Matches any flag, dedicated or overloaded
639pub(crate) fn flag<'s, I>() -> impl Parser<'s, I, Flag, Extra<'s>>
640where
641    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
642{
643    let dedicated_flag = select_ref!(Token::DedicatedFlag(flag) => *flag);
644
645    choice((
646        dedicated_flag,
647        just(Token::Asterisk).to(Flag::Asterisk),
648        just(Token::Hash).to(Flag::Hash),
649    ))
650}
651
652/// Matches a [Posting] complete with [Metadata] over several lines.
653fn posting<'s, I>() -> impl Parser<'s, I, Spanned<Posting<'s>>, Extra<'s>>
654where
655    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
656{
657    just(Token::Indent)
658        .ignore_then(
659            group((
660                flag().map_with(spanned_extra).or_not(),
661                account().map_with(spanned_extra),
662                expr_value().map_with(spanned_extra).or_not(),
663                currency().map_with(spanned_extra).or_not(),
664                cost_spec().or_not().map_with(|cost_spec, e| {
665                    cost_spec.map(|cost_spec| spanned(cost_spec, e.span()))
666                }),
667                price_annotation().or_not().map_with(|price_spec, e| {
668                    price_spec.map(|price_spec| spanned(price_spec, e.span()))
669                }),
670            ))
671            .map_with(spanned_extra)
672            .then_ignore(just(Token::Eol))
673            .then(metadata().map_with(spanned_extra))
674            .map(
675                |(
676                    Spanned {
677                        item: (flag, account, amount, currency, cost_spec, price_annotation),
678                        span: posting_span_without_metadata,
679                    },
680                    metadata,
681                )| {
682                    spanned(
683                        Posting {
684                            flag,
685                            account,
686                            amount,
687                            currency,
688                            cost_spec,
689                            price_annotation,
690                            metadata,
691                        },
692                        posting_span_without_metadata,
693                    )
694                },
695            ),
696        )
697        .labelled("posting")
698        .as_context()
699}
700
701/// Matches [Metadata], over several lines.
702fn metadata<'s, I>() -> impl Parser<'s, I, Metadata<'s>, Extra<'s>>
703where
704    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
705{
706    use Metadatum::*;
707
708    metadatum_line()
709        .repeated()
710        .collect::<Vec<_>>()
711        .validate(|metadata, _span, emitter| {
712            // collate by type of metadatum
713            metadata
714                .into_iter()
715                .fold(Metadata::default(), |mut m, item| match item {
716                    KeyValue(kv) => {
717                        use hash_map::Entry::*;
718
719                        let MetaKeyValue { key, value } = kv.item;
720
721                        let key_span = key.span;
722                        match m.key_values.entry(key) {
723                            Occupied(entry) => emitter.emit(Rich::custom(
724                                key_span,
725                                format!("duplicate key {}", entry.key()),
726                            )),
727                            Vacant(entry) => {
728                                entry.insert(value);
729                            }
730                        }
731
732                        m
733                    }
734                    Tag(tag) => {
735                        if m.tags.contains(&tag) {
736                            emitter.emit(Rich::custom(tag.span, format!("duplicate tag {}", tag)))
737                        } else {
738                            m.tags.insert(tag);
739                        }
740
741                        m
742                    }
743                    Link(link) => {
744                        if m.links.contains(&link) {
745                            emitter
746                                .emit(Rich::custom(link.span, format!("duplicate link {}", link)))
747                        } else {
748                            m.links.insert(link);
749                        }
750
751                        m
752                    }
753                })
754        })
755}
756
757/// A single instance of [Metadata]
758enum Metadatum<'a> {
759    KeyValue(Spanned<MetaKeyValue<'a>>),
760    Tag(Spanned<Tag<'a>>),
761    Link(Spanned<Link<'a>>),
762}
763
764/// Matches a single Metadatum on a single line.
765fn meta_key_value<'s, I>() -> impl Parser<'s, I, MetaKeyValue<'s>, Extra<'s>>
766where
767    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
768{
769    key()
770        .map_with(spanned_extra)
771        .then(just(Token::Colon).ignore_then(meta_value().or_not().map_with(spanned_extra)))
772        .map(|(key, value)| MetaKeyValue {
773            key,
774            value: value.map_into(|value| value.unwrap_or(MetaValue::Simple(SimpleValue::Null))),
775        })
776}
777
778/// Matches a single Metadatum on a single line.
779fn metadatum_line<'s, I>() -> impl Parser<'s, I, Metadatum<'s>, Extra<'s>>
780where
781    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
782{
783    use Metadatum::*;
784
785    just(Token::Indent)
786        .ignore_then(
787            choice((
788                meta_key_value().map_with(spanned_extra).map(KeyValue),
789                tag().map_with(spanned_extra).map(Tag),
790                link().map_with(spanned_extra).map(Link),
791            ))
792            .then_ignore(just(Token::Eol)),
793        )
794        .labelled("metadata")
795        .as_context()
796}
797
798/// Matches a non-empty [MetaValue].
799pub(crate) fn meta_value<'s, I>() -> impl Parser<'s, I, MetaValue<'s>, Extra<'s>>
800where
801    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
802{
803    use MetaValue::*;
804
805    // try for amount first
806    choice((amount().map(Amount), simple_value().map(Simple)))
807}
808
809/// Matches a non-empty [SimpleValue].
810pub(crate) fn simple_value<'s, I>() -> impl Parser<'s, I, SimpleValue<'s>, Extra<'s>>
811where
812    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
813{
814    use SimpleValue::*;
815
816    choice((
817        string().map(String),
818        currency().map(Currency),
819        account().map(Account),
820        tag().map(Tag),
821        link().map(Link),
822        date().map(Date),
823        bool().map(Bool),
824        just(Token::Null).to(Null),
825        expr_value().map(Expr),
826    ))
827}
828
829pub(crate) fn amount<'s, I>() -> impl Parser<'s, I, Amount<'s>, Extra<'s>>
830where
831    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
832{
833    group((
834        expr_value().map_with(spanned_extra),
835        currency().map_with(spanned_extra),
836    ))
837    .map(Amount::new)
838}
839
840pub(crate) fn amount_with_tolerance<'s, I>(
841) -> impl Parser<'s, I, AmountWithTolerance<'s>, Extra<'s>>
842where
843    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
844{
845    choice((
846        amount().map_with(|amount, e| AmountWithTolerance::new((spanned_extra(amount, e), None))),
847        group((
848            expr_value().map_with(spanned_extra),
849            just(Token::Tilde),
850            decimal().map_with(spanned_extra),
851            currency().map_with(spanned_extra),
852        ))
853        .map_with(|(number, _, tolerance, currency), e| {
854            AmountWithTolerance::new((
855                spanned_extra(Amount::new((number, currency)), e),
856                Some(tolerance),
857            ))
858        }),
859    ))
860}
861
862pub(crate) fn loose_amount<'s, I>() -> impl Parser<'s, I, LooseAmount<'s>, Extra<'s>>
863where
864    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
865{
866    group((
867        expr_value().map_with(spanned_extra).or_not(),
868        currency().map_with(spanned_extra).or_not(),
869    ))
870    .map(LooseAmount::new)
871}
872
873pub(crate) fn compound_amount<'s, I>() -> impl Parser<'s, I, CompoundAmount<'s>, Extra<'s>>
874where
875    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
876{
877    use CompoundAmount::*;
878
879    choice((
880        (compound_expr().then(currency())).map(|(amount, cur)| CurrencyAmount(amount, cur)),
881        compound_expr().map(BareAmount),
882        just(Token::Hash) // bare currency may or may not be preceeded by hash
883            .or_not()
884            .ignore_then(currency().map(BareCurrency)),
885    ))
886}
887
888pub(crate) fn compound_expr<'s, I>() -> impl Parser<'s, I, CompoundExprValue, Extra<'s>>
889where
890    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
891{
892    use CompoundExprValue::*;
893
894    choice((
895        // try for both per-unit and total first
896        expr_value()
897            .then_ignore(just(Token::Hash))
898            .then(expr_value())
899            .map(|(per_unit, total)| PerUnitAndTotal(per_unit, total)),
900        expr_value().then_ignore(just(Token::Hash)).map(PerUnit),
901        expr_value().map(PerUnit),
902        just(Token::Hash).ignore_then(expr_value()).map(Total),
903    ))
904}
905
906pub(crate) fn scoped_expr<'s, I>() -> impl Parser<'s, I, ScopedExprValue, Extra<'s>>
907where
908    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
909{
910    use ScopedExprValue::*;
911
912    choice((
913        expr_value().then_ignore(just(Token::Hash)).map(PerUnit),
914        expr_value().map(PerUnit),
915        just(Token::Hash).ignore_then(expr_value()).map(Total),
916    ))
917}
918
919pub(crate) fn price_annotation<'s, I>() -> impl Parser<'s, I, PriceSpec<'s>, Extra<'s>>
920where
921    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
922{
923    use PriceSpec::*;
924
925    fn scope(amount: ExprValue, is_total: bool) -> ScopedExprValue {
926        use ScopedExprValue::*;
927
928        if is_total {
929            Total(amount)
930        } else {
931            PerUnit(amount)
932        }
933    }
934
935    group((
936        choice((just(Token::At).to(false), just(Token::AtAt).to(true))),
937        expr_value().or_not(),
938        currency().or_not(),
939    ))
940    .try_map(|(is_total, amount, cur), _span| match (amount, cur) {
941        (Some(amount), Some(cur)) => Ok(CurrencyAmount(scope(amount, is_total), cur)),
942        (Some(amount), None) => Ok(BareAmount(scope(amount, is_total))),
943        (None, Some(cur)) => Ok(BareCurrency(cur)),
944        (None, None) => Ok(Unspecified),
945    })
946}
947
948/// Matches a [CostSpec].
949/// For now we only match the new syntax of single braces.
950fn cost_spec<'s, I>() -> impl Parser<'s, I, CostSpec<'s>, Extra<'s>>
951where
952    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
953{
954    use self::CompoundAmount::*;
955    use CostComp::*;
956
957    just(Token::Lcurl)
958        .ignore_then(
959            group((
960                cost_comp().map_with(spanned_extra),
961                (just(Token::Comma).ignore_then(cost_comp().map_with(spanned_extra)))
962                    .repeated()
963                    .collect::<Vec<_>>(),
964            ))
965            .or_not(), // allow for empty cost spec
966        )
967        .then_ignore(just(Token::Rcurl))
968        .try_map(move |cost_spec, span| {
969            let mut builder = match cost_spec {
970                Some((head, tail)) => {
971                    once(head).chain(tail).fold(
972                        // accumulate the `CostComp`s in a `CostSpecBuilder`
973                        CostSpecBuilder::default(),
974                        |builder, cost_comp| match cost_comp.item {
975                            CompoundAmount(compound_amount) => match compound_amount {
976                                BareCurrency(cur) => builder.currency(cur, cost_comp.span),
977                                BareAmount(amount) => builder.compound_expr(amount, cost_comp.span),
978                                CurrencyAmount(amount, cur) => builder
979                                    .compound_expr(amount, cost_comp.span)
980                                    .currency(cur, cost_comp.span),
981                            },
982                            Date(date) => builder.date(date, cost_comp.span),
983                            Label(s) => builder.label(s, cost_comp.span),
984                            Merge => builder.merge(cost_comp.span),
985                        },
986                    )
987                }
988                None => CostSpecBuilder::default(),
989            };
990            builder
991                .build()
992                .map_err(|e| Rich::custom(span, e.to_string()))
993        })
994}
995
996#[derive(PartialEq, Eq, Clone, Debug)]
997/// One component of a cost specification.
998/// Setting a field type multiple times is rejected by methods in [CostSpec].
999enum CostComp<'a> {
1000    CompoundAmount(CompoundAmount<'a>),
1001    Date(Date),
1002    Label(&'a str),
1003    Merge,
1004}
1005
1006/// Matches one component of a [CostSpec].
1007fn cost_comp<'s, I>() -> impl Parser<'s, I, CostComp<'s>, Extra<'s>>
1008where
1009    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1010{
1011    use CostComp::*;
1012
1013    choice((
1014        compound_amount().map(CompoundAmount),
1015        date().map(Date),
1016        string().map(Label),
1017        just(Token::Asterisk).to(Merge),
1018    ))
1019}
1020
1021/// Matches zero or more tags or links.
1022/// Duplicates are errors.
1023pub(crate) fn tags_links<'s, I>(
1024) -> impl Parser<'s, I, (HashSet<Spanned<Tag<'s>>>, HashSet<Spanned<Link<'s>>>), Extra<'s>>
1025where
1026    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1027{
1028    choice((
1029        tag().map_with(spanned_extra).map(Either::Left),
1030        link().map_with(spanned_extra).map(Either::Right),
1031    ))
1032    .repeated()
1033    .collect::<Vec<_>>()
1034    .validate(|tags_or_links, _span, emitter| {
1035        tags_or_links.into_iter().fold(
1036            (HashSet::new(), HashSet::new()),
1037            |(mut tags, mut links), item| match item {
1038                Either::Left(tag) => {
1039                    if tags.contains(&tag) {
1040                        emitter.emit(Rich::custom(tag.span, format!("duplicate tag {}", tag)))
1041                    } else {
1042                        tags.insert(tag);
1043                    }
1044
1045                    (tags, links)
1046                }
1047                Either::Right(link) => {
1048                    if links.contains(&link) {
1049                        emitter.emit(Rich::custom(link.span, format!("duplicate link {}", link)))
1050                    } else {
1051                        links.insert(link);
1052                    }
1053
1054                    (tags, links)
1055                }
1056            },
1057        )
1058    })
1059}
1060
1061/// Matches a bool
1062pub(crate) fn bool<'s, I>() -> impl Parser<'s, I, bool, Extra<'s>>
1063where
1064    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1065{
1066    choice((just(Token::True).to(true), just(Token::False).to(false)))
1067}
1068
1069/// Match and evaluate an expression
1070pub(crate) fn expr_value<'s, I>() -> impl Parser<'s, I, ExprValue, Extra<'s>>
1071where
1072    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1073{
1074    expr().map(ExprValue::from)
1075}
1076
1077/// Match an expression
1078pub(crate) fn expr<'s, I>() -> impl Parser<'s, I, Expr, Extra<'s>>
1079where
1080    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1081{
1082    use Token::*;
1083
1084    recursive(|expr| {
1085        // Match a parenthesized expression
1086        let parens = expr
1087            .clone()
1088            .delimited_by(just(Lparen), just(Rparen))
1089            .map(|x| Expr::Paren(Box::new(x)));
1090
1091        // Match a bare number
1092        let number = select_ref! { Number(x) => Expr::Value(*x) };
1093
1094        // Match a factor of an expression
1095        let factor = choice((just(Minus), just(Plus)))
1096            .or_not()
1097            .then(number.or(parens.clone()))
1098            .map(|(negated, x)| {
1099                if negated.is_some_and(|tok| tok == Minus) {
1100                    Expr::Neg(Box::new(x))
1101                } else {
1102                    x
1103                }
1104            });
1105
1106        // Match a product of factors
1107        let product = factor.clone().foldl(
1108            choice((
1109                just(Asterisk).to(Expr::Mul as fn(_, _) -> _),
1110                just(Slash).to(Expr::Div as fn(_, _) -> _),
1111            ))
1112            .then(factor.clone())
1113            .repeated(),
1114            |lhs, (op, rhs)| op(Box::new(lhs), Box::new(rhs)),
1115        );
1116
1117        // Match an expression
1118        product.clone().foldl(
1119            choice((
1120                just(Plus).to(Expr::Add as fn(_, _) -> _),
1121                just(Minus).to(Expr::Sub as fn(_, _) -> _),
1122            ))
1123            .then(product.clone())
1124            .repeated(),
1125            |lhs, (op, rhs)| op(Box::new(lhs), Box::new(rhs)),
1126        )
1127    })
1128}
1129
1130/// Matches a Tag
1131fn tag<'s, I>() -> impl Parser<'s, I, Tag<'s>, Extra<'s>>
1132where
1133    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1134{
1135    let tag = select_ref!(Token::Tag(s) => *s);
1136    tag.try_map(|s, span| Tag::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1137}
1138
1139/// Matches a Link
1140fn link<'s, I>() -> impl Parser<'s, I, Link<'s>, Extra<'s>>
1141where
1142    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1143{
1144    let link = select_ref!(Token::Link(s) => *s);
1145    link.try_map(|s, span| Link::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1146}
1147
1148/// Matches a Key.
1149/// Note that we may have to hijack another token and use it as a key,
1150/// since keywords do get used as metadata keys.
1151fn key<'s, I>() -> impl Parser<'s, I, Key<'s>, Extra<'s>>
1152where
1153    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1154{
1155    let key = select_ref!(Token::Key(s) => *s);
1156
1157    key.try_map(|s, span| Key::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1158}
1159
1160/// Matches a Currency
1161fn currency<'s, I>() -> impl Parser<'s, I, Currency<'s>, Extra<'s>>
1162where
1163    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1164{
1165    let currency = select_ref!(Token::Currency(s) => *s);
1166    currency.try_map(|s, span| Currency::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1167}
1168
1169/// Matches a Date
1170fn date<'s, I>() -> impl Parser<'s, I, Date, Extra<'s>>
1171where
1172    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1173{
1174    select_ref!(Token::Date(date) => *date)
1175}
1176
1177/// Matches a Decimal
1178fn decimal<'s, I>() -> impl Parser<'s, I, Decimal, Extra<'s>>
1179where
1180    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1181{
1182    select_ref!(Token::Number(x) => *x)
1183}
1184
1185/// Matches a string
1186fn string<'s, I>() -> impl Parser<'s, I, &'s str, Extra<'s>>
1187where
1188    I: BorrowInput<'s, Token = Token<'s>, Span = Span>,
1189{
1190    let string = select_ref!(Token::StringLiteral(s) => s.deref());
1191
1192    string.map_with(|s, e| {
1193        let span = e.span();
1194        let simple_state: &mut extra::SimpleState<ParserState> = e.state();
1195        let parser_state: &mut ParserState = simple_state;
1196        let ParserState { warnings, options } = parser_state;
1197        let line_count = s.chars().filter(|c| *c == '\n').count() + 1;
1198        let long_string_maxlines = options.long_string_maxlines.as_ref().map(|n| *n.item()).unwrap_or(DEFAULT_LONG_STRING_MAXLINES);
1199        if line_count > long_string_maxlines {
1200            let option_span = options.long_string_maxlines.as_ref().map(|s| s.source.value);
1201            let is_default = option_span.is_none();
1202            let warning = Warning::new(
1203                "string too long",
1204                format!(
1205                    "exceeds long_string_maxlines({}{}) - hint: would require option \"long_string_maxlines\" \"{}\"",
1206                    if is_default { "default " } else { "" },
1207                    long_string_maxlines,
1208                    line_count
1209                ),
1210                span,
1211            );
1212
1213            if let Some(option_span) = option_span {
1214                warnings.push(warning.related_to_named_span("max allowed", option_span));
1215            } else {
1216                warnings.push(warning)
1217            }
1218        }
1219        s
1220    })
1221}
1222
1223impl<'a> Metadata<'a> {
1224    pub(crate) fn merge_tags<E>(&mut self, tags: &HashSet<Spanned<Tag<'a>>>, emitter: &mut E)
1225    where
1226        E: Emit<ParserError<'a>>,
1227    {
1228        for tag in tags {
1229            match self.tags.get(tag) {
1230                None => {
1231                    self.tags.insert(*tag);
1232                }
1233                Some(existing_tag) => {
1234                    let error = Rich::custom(existing_tag.span, format!("duplicate tag {}", tag));
1235                    // TODO: label the error in context, type annotations need fixing for chumsky 1.0.0-alpha7 to alpha8 transition
1236                    // LabelError::<
1237                    //     chumsky::input::WithContext<
1238                    //         Span,
1239                    //         chumsky::input::SpannedInput<Token<'_>, Span, &[(Token<'_>, Span)]>,
1240                    //     >,
1241                    //     &str,
1242                    // >::in_context(&mut error, "tag", tag.span);
1243                    emitter.emit(error);
1244                }
1245            }
1246        }
1247    }
1248
1249    // Augment only for tags which are not already present, others silently ignored.
1250    // This is so that tags attached to directives take precedence over the push stack.
1251    pub(crate) fn augment_tags(&mut self, tags: &HashMap<Spanned<Tag<'a>>, Vec<Spanned<Tag<'a>>>>) {
1252        for (tag, spans) in tags.iter() {
1253            if !self.tags.contains(tag) {
1254                let most_recently_pushed_tag = spans.last().unwrap_or(tag);
1255                self.tags.insert(*most_recently_pushed_tag);
1256            }
1257        }
1258    }
1259
1260    pub(crate) fn merge_links<E>(&mut self, links: &HashSet<Spanned<Link<'a>>>, emitter: &mut E)
1261    where
1262        E: Emit<ParserError<'a>>,
1263    {
1264        for link in links {
1265            match self.links.get(link) {
1266                None => {
1267                    self.links.insert(*link);
1268                }
1269                Some(existing_link) => {
1270                    let error =
1271                        Rich::custom(existing_link.span, format!("duplicate link {}", link));
1272                    // TODO: label the error in context, type annotations need fixing for chumsky 1.0.0-alpha7 to alpha8 transition
1273                    // LabelError::<
1274                    //     chumsky::input::WithContext<
1275                    //         Span,
1276                    //         chumsky::input::SpannedInput<Token<'_>, Span, &[(Token<'_>, Span)]>,
1277                    //     >,
1278                    //     &str,
1279                    // >::in_context(&mut error, "link", link.span);
1280                    emitter.emit(error);
1281                }
1282            }
1283        }
1284    }
1285
1286    // Augment only for keys which are not already present, others silently ignored.
1287    // This is so that key/values attached to directives take precedence over the push stack.
1288    pub(crate) fn augment_key_values(
1289        &mut self,
1290        key_values: &HashMap<Spanned<Key<'a>>, Vec<(Span, Spanned<MetaValue<'a>>)>>,
1291    ) {
1292        for (key, values) in key_values {
1293            if !self.key_values.contains_key(key) {
1294                let (key_span, value) = values.last().unwrap();
1295                self.key_values.insert(
1296                    spanned(*key.item(), *key_span),
1297                    // Sadly we do have to clone the value here, so we can
1298                    // merge in metadata key/values from the push/pop stack
1299                    // without consuming it.
1300                    value.clone(),
1301                );
1302            }
1303        }
1304    }
1305}
1306
1307type ParserError<'a> = Rich<'a, Token<'a>, Span>;
1308
1309impl From<ParserError<'_>> for Error {
1310    fn from(error: ParserError) -> Self {
1311        let error = error.map_token(|tok| tok.to_string());
1312
1313        Error::with_contexts(
1314            error.to_string(),
1315            error.reason().to_string(),
1316            *error.span(),
1317            error
1318                .contexts()
1319                .map(|(label, span)| (label.to_string(), *span))
1320                .collect(),
1321        )
1322    }
1323}
1324
1325// the state we thread through the parsers
1326#[derive(Default, Debug)]
1327pub(crate) struct ParserState<'a> {
1328    pub(crate) options: ParserOptions<'a>,
1329    pub(crate) warnings: Vec<Warning>,
1330}
1331
1332// our ParserExtra with our error and state types
1333pub(crate) type Extra<'a> = extra::Full<ParserError<'a>, extra::SimpleState<ParserState<'a>>, ()>;
1334
1335/// Enable use of own functions which emit errors
1336pub(crate) trait Emit<E> {
1337    fn emit(&mut self, err: E);
1338}
1339
1340impl<E> Emit<E> for chumsky::input::Emitter<E> {
1341    fn emit(&mut self, err: E) {
1342        self.emit(err)
1343    }
1344}
1345
1346// simple collection of errors in a Vec
1347impl<E> Emit<E> for Vec<Error>
1348where
1349    E: Into<Error>,
1350{
1351    fn emit(&mut self, err: E) {
1352        self.push(err.into())
1353    }
1354}
1355// a degenerate error sink
1356struct NullEmitter;
1357
1358impl<E> Emit<E> for NullEmitter {
1359    fn emit(&mut self, _err: E) {}
1360}
1361
1362mod tests;