Skip to main content

beancount_parser_lima/
parsers.rs

1use crate::{
2    lexer::Token,
3    options::{BeancountOption, BeancountOptionError, DEFAULT_LONG_STRING_MAXLINES, ParserOptions},
4    types::*,
5};
6use chumsky::{
7    input::BorrowInput,
8    prelude::{
9        IterParser, Parser, Rich, any_ref, choice, end, extra, group, just, recursive, select_ref,
10        skip_then_retry_until,
11    },
12};
13use either::Either;
14use rust_decimal::Decimal;
15use std::{
16    collections::{HashMap, HashSet, hash_map},
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(choice((just(Token::Eol).ignored(), end())))
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.into(), e.to_string()),
147                    BadValue(_) => Rich::custom(value.span.into(), 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.into(), 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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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.into(),
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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(choice((just(Token::Eol).ignored(), end())))
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.into(),
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(
737                                tag.span.into(),
738                                format!("duplicate tag {}", tag),
739                            ))
740                        } else {
741                            m.tags.insert(tag);
742                        }
743
744                        m
745                    }
746                    Link(link) => {
747                        if m.links.contains(&link) {
748                            emitter.emit(Rich::custom(
749                                link.span.into(),
750                                format!("duplicate link {}", link),
751                            ))
752                        } else {
753                            m.links.insert(link);
754                        }
755
756                        m
757                    }
758                })
759        })
760}
761
762/// A single instance of [Metadata]
763enum Metadatum<'a> {
764    KeyValue(Spanned<MetaKeyValue<'a>>),
765    Tag(Spanned<Tag<'a>>),
766    Link(Spanned<Link<'a>>),
767}
768
769/// Matches a single Metadatum on a single line.
770fn meta_key_value<'s, I>() -> impl Parser<'s, I, MetaKeyValue<'s>, Extra<'s>>
771where
772    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
773{
774    key()
775        .map_with(spanned_extra)
776        .then(just(Token::Colon).ignore_then(meta_value().or_not().map_with(spanned_extra)))
777        .map(|(key, value)| MetaKeyValue {
778            key,
779            value: value.map_into(|value| value.unwrap_or(MetaValue::Simple(SimpleValue::Null))),
780        })
781}
782
783/// Matches a single Metadatum on a single line.
784fn metadatum_line<'s, I>() -> impl Parser<'s, I, Metadatum<'s>, Extra<'s>>
785where
786    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
787{
788    use Metadatum::*;
789
790    just(Token::Indent)
791        .ignore_then(
792            choice((
793                meta_key_value().map_with(spanned_extra).map(KeyValue),
794                tag().map_with(spanned_extra).map(Tag),
795                link().map_with(spanned_extra).map(Link),
796            ))
797            .then_ignore(choice((just(Token::Eol).ignored(), end()))),
798        )
799        .labelled("metadata")
800        .as_context()
801}
802
803/// Matches a non-empty [MetaValue].
804pub(crate) fn meta_value<'s, I>() -> impl Parser<'s, I, MetaValue<'s>, Extra<'s>>
805where
806    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
807{
808    use MetaValue::*;
809
810    // try for amount first
811    choice((amount().map(Amount), simple_value().map(Simple)))
812}
813
814/// Matches a non-empty [SimpleValue].
815pub(crate) fn simple_value<'s, I>() -> impl Parser<'s, I, SimpleValue<'s>, Extra<'s>>
816where
817    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
818{
819    use SimpleValue::*;
820
821    choice((
822        string().map(String),
823        currency().map(Currency),
824        account().map(Account),
825        tag().map(Tag),
826        link().map(Link),
827        date().map(Date),
828        bool().map(Bool),
829        just(Token::Null).to(Null),
830        expr_value().map(Expr),
831    ))
832}
833
834pub(crate) fn amount<'s, I>() -> impl Parser<'s, I, Amount<'s>, Extra<'s>>
835where
836    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
837{
838    group((
839        expr_value().map_with(spanned_extra),
840        currency().map_with(spanned_extra),
841    ))
842    .map(Amount::new)
843}
844
845pub(crate) fn amount_with_tolerance<'s, I>()
846-> impl Parser<'s, I, AmountWithTolerance<'s>, Extra<'s>>
847where
848    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
849{
850    choice((
851        amount().map_with(|amount, e| AmountWithTolerance::new((spanned_extra(amount, e), None))),
852        group((
853            expr_value().map_with(spanned_extra),
854            just(Token::Tilde),
855            decimal().map_with(spanned_extra),
856            currency().map_with(spanned_extra),
857        ))
858        .map_with(|(number, _, tolerance, currency), e| {
859            AmountWithTolerance::new((
860                spanned_extra(Amount::new((number, currency)), e),
861                Some(tolerance),
862            ))
863        }),
864    ))
865}
866
867pub(crate) fn loose_amount<'s, I>() -> impl Parser<'s, I, LooseAmount<'s>, Extra<'s>>
868where
869    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
870{
871    group((
872        expr_value().map_with(spanned_extra).or_not(),
873        currency().map_with(spanned_extra).or_not(),
874    ))
875    .map(LooseAmount::new)
876}
877
878pub(crate) fn compound_amount<'s, I>() -> impl Parser<'s, I, CompoundAmount<'s>, Extra<'s>>
879where
880    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
881{
882    use CompoundAmount::*;
883
884    choice((
885        (compound_expr().then(currency())).map(|(amount, cur)| CurrencyAmount(amount, cur)),
886        compound_expr().map(BareAmount),
887        just(Token::Hash) // bare currency may or may not be preceeded by hash
888            .or_not()
889            .ignore_then(currency().map(BareCurrency)),
890    ))
891}
892
893pub(crate) fn compound_expr<'s, I>() -> impl Parser<'s, I, CompoundExprValue, Extra<'s>>
894where
895    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
896{
897    use CompoundExprValue::*;
898
899    choice((
900        // try for both per-unit and total first
901        expr_value()
902            .then_ignore(just(Token::Hash))
903            .then(expr_value())
904            .map(|(per_unit, total)| PerUnitAndTotal(per_unit, total)),
905        expr_value().then_ignore(just(Token::Hash)).map(PerUnit),
906        expr_value().map(PerUnit),
907        just(Token::Hash).ignore_then(expr_value()).map(Total),
908    ))
909}
910
911pub(crate) fn scoped_expr<'s, I>() -> impl Parser<'s, I, ScopedExprValue, Extra<'s>>
912where
913    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
914{
915    use ScopedExprValue::*;
916
917    choice((
918        expr_value().then_ignore(just(Token::Hash)).map(PerUnit),
919        expr_value().map(PerUnit),
920        just(Token::Hash).ignore_then(expr_value()).map(Total),
921    ))
922}
923
924pub(crate) fn price_annotation<'s, I>() -> impl Parser<'s, I, PriceSpec<'s>, Extra<'s>>
925where
926    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
927{
928    use PriceSpec::*;
929
930    fn scope(amount: ExprValue, is_total: bool) -> ScopedExprValue {
931        use ScopedExprValue::*;
932
933        if is_total {
934            Total(amount)
935        } else {
936            PerUnit(amount)
937        }
938    }
939
940    group((
941        choice((just(Token::At).to(false), just(Token::AtAt).to(true))),
942        expr_value().or_not(),
943        currency().or_not(),
944    ))
945    .try_map(|(is_total, amount, cur), _span| match (amount, cur) {
946        (Some(amount), Some(cur)) => Ok(CurrencyAmount(scope(amount, is_total), cur)),
947        (Some(amount), None) => Ok(BareAmount(scope(amount, is_total))),
948        (None, Some(cur)) => Ok(BareCurrency(cur)),
949        (None, None) => Ok(Unspecified),
950    })
951}
952
953/// Matches a [CostSpec].
954/// For now we only match the new syntax of single braces.
955fn cost_spec<'s, I>() -> impl Parser<'s, I, CostSpec<'s>, Extra<'s>>
956where
957    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
958{
959    use self::CompoundAmount::*;
960    use CostComp::*;
961
962    just(Token::Lcurl)
963        .ignore_then(
964            group((
965                cost_comp().map_with(spanned_extra),
966                (just(Token::Comma).ignore_then(cost_comp().map_with(spanned_extra)))
967                    .repeated()
968                    .collect::<Vec<_>>(),
969            ))
970            .or_not(), // allow for empty cost spec
971        )
972        .then_ignore(just(Token::Rcurl))
973        .try_map(move |cost_spec, span| {
974            let mut builder = match cost_spec {
975                Some((head, tail)) => {
976                    once(head).chain(tail).fold(
977                        // accumulate the `CostComp`s in a `CostSpecBuilder`
978                        CostSpecBuilder::default(),
979                        |builder, cost_comp| match cost_comp.item {
980                            CompoundAmount(compound_amount) => match compound_amount {
981                                BareCurrency(cur) => builder.currency(cur, cost_comp.span),
982                                BareAmount(amount) => builder.compound_expr(amount, cost_comp.span),
983                                CurrencyAmount(amount, cur) => builder
984                                    .compound_expr(amount, cost_comp.span)
985                                    .currency(cur, cost_comp.span),
986                            },
987                            Date(date) => builder.date(date, cost_comp.span),
988                            Label(s) => builder.label(s, cost_comp.span),
989                            Merge => builder.merge(cost_comp.span),
990                        },
991                    )
992                }
993                None => CostSpecBuilder::default(),
994            };
995            builder
996                .build()
997                .map_err(|e| Rich::custom(span, e.to_string()))
998        })
999}
1000
1001#[derive(PartialEq, Eq, Clone, Debug)]
1002/// One component of a cost specification.
1003/// Setting a field type multiple times is rejected by methods in [CostSpec].
1004enum CostComp<'a> {
1005    CompoundAmount(CompoundAmount<'a>),
1006    Date(Date),
1007    Label(&'a str),
1008    Merge,
1009}
1010
1011/// Matches one component of a [CostSpec].
1012fn cost_comp<'s, I>() -> impl Parser<'s, I, CostComp<'s>, Extra<'s>>
1013where
1014    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1015{
1016    use CostComp::*;
1017
1018    choice((
1019        compound_amount().map(CompoundAmount),
1020        date().map(Date),
1021        string().map(Label),
1022        just(Token::Asterisk).to(Merge),
1023    ))
1024}
1025
1026/// Matches zero or more tags or links.
1027/// Duplicates are errors.
1028pub(crate) fn tags_links<'s, I>()
1029-> impl Parser<'s, I, (HashSet<Spanned<Tag<'s>>>, HashSet<Spanned<Link<'s>>>), Extra<'s>>
1030where
1031    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1032{
1033    choice((
1034        tag().map_with(spanned_extra).map(Either::Left),
1035        link().map_with(spanned_extra).map(Either::Right),
1036    ))
1037    .repeated()
1038    .collect::<Vec<_>>()
1039    .validate(|tags_or_links, _span, emitter| {
1040        tags_or_links.into_iter().fold(
1041            (HashSet::new(), HashSet::new()),
1042            |(mut tags, mut links), item| match item {
1043                Either::Left(tag) => {
1044                    if tags.contains(&tag) {
1045                        emitter.emit(Rich::custom(
1046                            tag.span.into(),
1047                            format!("duplicate tag {}", tag),
1048                        ))
1049                    } else {
1050                        tags.insert(tag);
1051                    }
1052
1053                    (tags, links)
1054                }
1055                Either::Right(link) => {
1056                    if links.contains(&link) {
1057                        emitter.emit(Rich::custom(
1058                            link.span.into(),
1059                            format!("duplicate link {}", link),
1060                        ))
1061                    } else {
1062                        links.insert(link);
1063                    }
1064
1065                    (tags, links)
1066                }
1067            },
1068        )
1069    })
1070}
1071
1072/// Matches a bool
1073pub(crate) fn bool<'s, I>() -> impl Parser<'s, I, bool, Extra<'s>>
1074where
1075    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1076{
1077    choice((just(Token::True).to(true), just(Token::False).to(false)))
1078}
1079
1080/// Match and evaluate an expression
1081pub(crate) fn expr_value<'s, I>() -> impl Parser<'s, I, ExprValue, Extra<'s>>
1082where
1083    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1084{
1085    expr().map(ExprValue::from)
1086}
1087
1088/// Match an expression
1089pub(crate) fn expr<'s, I>() -> impl Parser<'s, I, Expr, Extra<'s>>
1090where
1091    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1092{
1093    use Token::*;
1094
1095    recursive(|expr| {
1096        // Match a parenthesized expression
1097        let parens = expr
1098            .clone()
1099            .delimited_by(just(Lparen), just(Rparen))
1100            .map(|x| Expr::Paren(Box::new(x)));
1101
1102        // Match a bare number
1103        let number = select_ref! { Number(x) => Expr::Value(*x) };
1104
1105        // Match a factor of an expression
1106        let factor = choice((just(Minus), just(Plus)))
1107            .or_not()
1108            .then(number.or(parens.clone()))
1109            .map(|(negated, x)| {
1110                if negated.is_some_and(|tok| tok == Minus) {
1111                    Expr::Neg(Box::new(x))
1112                } else {
1113                    x
1114                }
1115            });
1116
1117        // Match a product of factors
1118        let product = factor.clone().foldl(
1119            choice((
1120                just(Asterisk).to(Expr::Mul as fn(_, _) -> _),
1121                just(Slash).to(Expr::Div as fn(_, _) -> _),
1122            ))
1123            .then(factor.clone())
1124            .repeated(),
1125            |lhs, (op, rhs)| op(Box::new(lhs), Box::new(rhs)),
1126        );
1127
1128        // Match an expression
1129        product.clone().foldl(
1130            choice((
1131                just(Plus).to(Expr::Add as fn(_, _) -> _),
1132                just(Minus).to(Expr::Sub as fn(_, _) -> _),
1133            ))
1134            .then(product.clone())
1135            .repeated(),
1136            |lhs, (op, rhs)| op(Box::new(lhs), Box::new(rhs)),
1137        )
1138    })
1139}
1140
1141/// Matches a Tag
1142fn tag<'s, I>() -> impl Parser<'s, I, Tag<'s>, Extra<'s>>
1143where
1144    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1145{
1146    let tag = select_ref!(Token::Tag(s) => *s);
1147    tag.try_map(|s, span| Tag::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1148}
1149
1150/// Matches a Link
1151fn link<'s, I>() -> impl Parser<'s, I, Link<'s>, Extra<'s>>
1152where
1153    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1154{
1155    let link = select_ref!(Token::Link(s) => *s);
1156    link.try_map(|s, span| Link::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1157}
1158
1159/// Matches a Key.
1160/// Note that we may have to hijack another token and use it as a key,
1161/// since keywords do get used as metadata keys.
1162fn key<'s, I>() -> impl Parser<'s, I, Key<'s>, Extra<'s>>
1163where
1164    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1165{
1166    let key = select_ref!(Token::Key(s) => *s);
1167
1168    key.try_map(|s, span| Key::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1169}
1170
1171/// Matches a Currency
1172fn currency<'s, I>() -> impl Parser<'s, I, Currency<'s>, Extra<'s>>
1173where
1174    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1175{
1176    let currency = select_ref!(Token::Currency(s) => *s);
1177    currency.try_map(|s, span| Currency::try_from(s).map_err(|e| Rich::custom(span, e.to_string())))
1178}
1179
1180/// Matches a Date
1181fn date<'s, I>() -> impl Parser<'s, I, Date, Extra<'s>>
1182where
1183    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1184{
1185    select_ref!(Token::Date(date) => *date)
1186}
1187
1188/// Matches a Decimal
1189fn decimal<'s, I>() -> impl Parser<'s, I, Decimal, Extra<'s>>
1190where
1191    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1192{
1193    select_ref!(Token::Number(x) => *x)
1194}
1195
1196/// Matches a string
1197fn string<'s, I>() -> impl Parser<'s, I, &'s str, Extra<'s>>
1198where
1199    I: BorrowInput<'s, Token = Token<'s>, Span = Span_>,
1200{
1201    let string = select_ref!(Token::StringLiteral(s) => s.deref());
1202
1203    string.map_with(|s, e| {
1204        let span: Span_ = e.span();
1205        let simple_state: &mut extra::SimpleState<ParserState> = e.state();
1206        let parser_state: &mut ParserState = simple_state;
1207        let ParserState { warnings, options } = parser_state;
1208        let line_count = s.chars().filter(|c| *c == '\n').count() + 1;
1209        let long_string_maxlines = options.long_string_maxlines.as_ref().map(|n| *n.item()).unwrap_or(DEFAULT_LONG_STRING_MAXLINES);
1210        if line_count > long_string_maxlines {
1211            let option_span = options.long_string_maxlines.as_ref().map(|s| s.source.value);
1212            let is_default = option_span.is_none();
1213            let warning = Warning::new(
1214                "string too long",
1215                format!(
1216                    "exceeds long_string_maxlines({}{}) - hint: would require option \"long_string_maxlines\" \"{}\"",
1217                    if is_default { "default " } else { "" },
1218                    long_string_maxlines,
1219                    line_count
1220                ),
1221                span.into(),
1222            );
1223
1224            if let Some(option_span) = option_span {
1225                warnings.push(warning.related_to_named_span("max allowed", option_span));
1226            } else {
1227                warnings.push(warning)
1228            }
1229        }
1230        s
1231    })
1232}
1233
1234impl<'a> Metadata<'a> {
1235    pub(crate) fn merge_tags<E>(&mut self, tags: &HashSet<Spanned<Tag<'a>>>, emitter: &mut E)
1236    where
1237        E: Emit<ParserError<'a>>,
1238    {
1239        for tag in tags {
1240            match self.tags.get(tag) {
1241                None => {
1242                    self.tags.insert(*tag);
1243                }
1244                Some(existing_tag) => {
1245                    let error =
1246                        Rich::custom(existing_tag.span.into(), format!("duplicate tag {}", tag));
1247                    // TODO: label the error in context, type annotations need fixing for chumsky 1.0.0-alpha7 to alpha8 transition
1248                    // LabelError::<
1249                    //     chumsky::input::WithContext<
1250                    //         Span,
1251                    //         chumsky::input::SpannedInput<Token<'_>, Span, &[(Token<'_>, Span)]>,
1252                    //     >,
1253                    //     &str,
1254                    // >::in_context(&mut error, "tag", tag.span);
1255                    emitter.emit(error);
1256                }
1257            }
1258        }
1259    }
1260
1261    // Augment only for tags which are not already present, others silently ignored.
1262    // This is so that tags attached to directives take precedence over the push stack.
1263    pub(crate) fn augment_tags(&mut self, tags: &HashMap<Spanned<Tag<'a>>, Vec<Spanned<Tag<'a>>>>) {
1264        for (tag, spans) in tags.iter() {
1265            if !self.tags.contains(tag) {
1266                let most_recently_pushed_tag = spans.last().unwrap_or(tag);
1267                self.tags.insert(*most_recently_pushed_tag);
1268            }
1269        }
1270    }
1271
1272    pub(crate) fn merge_links<E>(&mut self, links: &HashSet<Spanned<Link<'a>>>, emitter: &mut E)
1273    where
1274        E: Emit<ParserError<'a>>,
1275    {
1276        for link in links {
1277            match self.links.get(link) {
1278                None => {
1279                    self.links.insert(*link);
1280                }
1281                Some(existing_link) => {
1282                    let error = Rich::custom(
1283                        existing_link.span.into(),
1284                        format!("duplicate link {}", link),
1285                    );
1286                    // TODO: label the error in context, type annotations need fixing for chumsky 1.0.0-alpha7 to alpha8 transition
1287                    // LabelError::<
1288                    //     chumsky::input::WithContext<
1289                    //         Span,
1290                    //         chumsky::input::SpannedInput<Token<'_>, Span, &[(Token<'_>, Span)]>,
1291                    //     >,
1292                    //     &str,
1293                    // >::in_context(&mut error, "link", link.span);
1294                    emitter.emit(error);
1295                }
1296            }
1297        }
1298    }
1299
1300    // Augment only for keys which are not already present, others silently ignored.
1301    // This is so that key/values attached to directives take precedence over the push stack.
1302    pub(crate) fn augment_key_values(
1303        &mut self,
1304        key_values: &HashMap<Spanned<Key<'a>>, Vec<(Span, Spanned<MetaValue<'a>>)>>,
1305    ) {
1306        for (key, values) in key_values {
1307            if !self.key_values.contains_key(key) {
1308                let (key_span, value) = values.last().unwrap();
1309                self.key_values.insert(
1310                    spanned(*key.item(), *key_span),
1311                    // Sadly we do have to clone the value here, so we can
1312                    // merge in metadata key/values from the push/pop stack
1313                    // without consuming it.
1314                    value.clone(),
1315                );
1316            }
1317        }
1318    }
1319}
1320
1321type ParserError<'a> = Rich<'a, Token<'a>, Span_>;
1322
1323impl From<ParserError<'_>> for Error {
1324    fn from(error: ParserError) -> Self {
1325        let error = error.map_token(|tok| tok.to_string());
1326
1327        Error::new(
1328            error.to_string(),
1329            error.reason().to_string(),
1330            error.span().into(),
1331        )
1332        .in_explicitly_labelled_contexts(
1333            error
1334                .contexts()
1335                .map(|(label, span)| (label.to_string(), span.into())),
1336        )
1337    }
1338}
1339
1340// the state we thread through the parsers
1341#[derive(Default, Debug)]
1342pub(crate) struct ParserState<'a> {
1343    pub(crate) options: ParserOptions<'a>,
1344    pub(crate) warnings: Vec<Warning>,
1345}
1346
1347// our ParserExtra with our error and state types
1348pub(crate) type Extra<'a> = extra::Full<ParserError<'a>, extra::SimpleState<ParserState<'a>>, ()>;
1349
1350/// Enable use of own functions which emit errors
1351pub(crate) trait Emit<E> {
1352    fn emit(&mut self, err: E);
1353}
1354
1355impl<E> Emit<E> for chumsky::input::Emitter<E> {
1356    fn emit(&mut self, err: E) {
1357        self.emit(err)
1358    }
1359}
1360
1361// simple collection of errors in a Vec
1362impl<E> Emit<E> for Vec<Error>
1363where
1364    E: Into<Error>,
1365{
1366    fn emit(&mut self, err: E) {
1367        self.push(err.into())
1368    }
1369}
1370// a degenerate error sink
1371struct NullEmitter;
1372
1373impl<E> Emit<E> for NullEmitter {
1374    fn emit(&mut self, _err: E) {}
1375}
1376
1377mod tests;