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
23pub(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
40pub(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
50pub(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
64pub(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
89pub(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") .as_context()
126}
127
128pub(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 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
164pub(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
202fn 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 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 (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
244pub(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
273pub(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
302pub(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
333fn 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
352fn 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(¤cy) {
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
384fn 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 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
402fn 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
410pub(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
437pub(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
464pub(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
492pub(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
520pub(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
548pub(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
579pub(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
607pub(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
630pub(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
638pub(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
652fn 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
701fn 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 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
757enum Metadatum<'a> {
759 KeyValue(Spanned<MetaKeyValue<'a>>),
760 Tag(Spanned<Tag<'a>>),
761 Link(Spanned<Link<'a>>),
762}
763
764fn 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
778fn 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
798pub(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 choice((amount().map(Amount), simple_value().map(Simple)))
807}
808
809pub(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) .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 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
948fn 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(), )
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 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)]
997enum CostComp<'a> {
1000 CompoundAmount(CompoundAmount<'a>),
1001 Date(Date),
1002 Label(&'a str),
1003 Merge,
1004}
1005
1006fn 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
1021pub(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
1061pub(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
1069pub(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
1077pub(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 let parens = expr
1087 .clone()
1088 .delimited_by(just(Lparen), just(Rparen))
1089 .map(|x| Expr::Paren(Box::new(x)));
1090
1091 let number = select_ref! { Number(x) => Expr::Value(*x) };
1093
1094 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 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 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
1130fn 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
1139fn 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
1148fn 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
1160fn 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
1169fn 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
1177fn 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
1185fn 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 emitter.emit(error);
1244 }
1245 }
1246 }
1247 }
1248
1249 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 emitter.emit(error);
1281 }
1282 }
1283 }
1284 }
1285
1286 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 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#[derive(Default, Debug)]
1327pub(crate) struct ParserState<'a> {
1328 pub(crate) options: ParserOptions<'a>,
1329 pub(crate) warnings: Vec<Warning>,
1330}
1331
1332pub(crate) type Extra<'a> = extra::Full<ParserError<'a>, extra::SimpleState<ParserState<'a>>, ()>;
1334
1335pub(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
1346impl<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}
1355struct NullEmitter;
1357
1358impl<E> Emit<E> for NullEmitter {
1359 fn emit(&mut self, _err: E) {}
1360}
1361
1362mod tests;