Expand description

beancount-parser-lima

A zero-copy parser for Beancount in Rust.

It is intended to be a complete implementation of the Beancount file format, except for those parts which are deprecated and other features as documented here (in a list which may not be comprehensive).

Currently under active development. APIs are subject to change, but I hope not majorly.

The slightly strange name is because of a somewhat careless failure on my part to notice the existing beancount-parser when starting this project, for which apologies.

(Note that zero-copy support from Chumsky is currently available only in alpha releases.)

Features

  • fast, thanks to Logos and Chumsky zero copy

  • beautiful error messages, thanks to Ariadne

  • interface for applications to also report beautiful errors in their original context, as in the example below

  • focus on conceptual clarity of application domain objects mapped to Rust types

Example application error messages

Roadmap

  • create Python bindings, so that this could be a drop-in replacement for the existing Beancount parser (which is not to say it will necessarily become that!)

  • improve API in the light of experience, i.e. when it gets some use 😅

  • address mistakes, misunderstandings, and edge-cases in the initial implementation as they are discovered

Uncertainties / TODOs

Yeah, Beancount is complicated, and I may have made some mistakes here. Current list of uncertainties, which is certainly not comprehensive.

  • metadata tags/links for a directive get folded in with those in the directive header line

Unsupported

This is an incomplete list of what is currently unsupported.

  • plugins

Unsupported Options

  • allow_pipe_separator
  • allow_deprecated_none_for_tags_and_links
  • default_tolerance
  • experiment_explicit_tolerances
  • insert_pythonpath
  • plugin
  • plugin_processing_mode
  • tolerance
  • use_legacy_fixed_tolerances

Also, unary options are not supported.

Alternatives

beancount-parser is another parser for Beancount which predates this one, using nom instead of Chumsky.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Examples

This example generates the output as shown above.


use beancount_parser_lima::{
    BeancountParser, BeancountSources, DirectiveVariant, ParseError, ParseResult,
};

fn main() {
    let stderr = &io::stderr();
    let sources = BeancountSources::new(PathBuf::from("examples/data/error-post-balancing.beancount"));
    let beancount_parser = BeancountParser::new(&sources);

    match beancount_parser.parse() {
        Ok(ParseResult {
            directives,
            options: _,
            mut warnings,
        }) => {
            let mut errors = Vec::new();

            for directive in directives {
                if let DirectiveVariant::Transaction(transaction) = directive.variant() {
                    let mut postings = transaction.postings().collect::<Vec<_>>();
                    let n_postings = postings.len();
                    let n_amounts = itertools::partition(&mut postings, |p| p.amount().is_some());

                    if postings.is_empty() {
                        warnings.push(directive.warning("no postings"));
                    } else if n_amounts + 1 < n_postings {
                        errors.push(
                            directive
                                .error("multiple postings without amount specified")
                                .related_to_all(postings[n_amounts..].iter().copied()),
                        );
                    } else if n_amounts == n_postings {
                        let total: Decimal =
                            postings.iter().map(|p| p.amount().unwrap().value()).sum();

                        if total != Decimal::ZERO {
                            let last_amount = postings.pop().unwrap().amount().unwrap();
                            let other_amounts = postings.iter().map(|p| p.amount().unwrap());

                            errors.push(
                                last_amount
                                    .error(format!("sum is {}, expected zero", total))
                                    .related_to_all(other_amounts)
                                    .in_context(&directive),
                            )
                        }
                    }
                }
            }

            sources.write(stderr, errors).unwrap();
            sources.write(stderr, warnings).unwrap();
        }

        Err(ParseError { errors, warnings }) => {
            sources.write(stderr, errors).unwrap();
            sources.write(stderr, warnings).unwrap();
        }
    }
}

Re-exports

Modules

Structs

  • The Beancount parser itself, which tokenizes and parses the source files contained in BeancountSources.
  • Contains the content of the Beancount source file, and the content of the transitive closure of all the include’d source files.
  • All options read in from option pragmas, excluding those for internal processing only.
  • The value returned when parsing fails.
  • The result of parsing all the files, containing date-ordered Directives, Options, and any Warnings.