NimbleParsec

repository·master·Indexed 21 days ago

https://github.com/dashbitco/nimble_parsec

A high-performance library for text-based parser combinators in Elixir. It compiles combinators into optimized Erlang VM binary matching clauses, providing performance comparable to hand-written parsers with no runtime dependency on the library after compilation. It includes tools for defining public and private parsers via defparsec/3 and defparsecp/3, reusable combinators with defcombinatorp/3, and a utility to generate random parseable binaries via generate/1.

Tokens
5.5K
Snippets
29
Records
29
Agent score
25%

What's inside NimbleParsec

  1. Optimize compile-time performance with defcombinatorp/3

    master

    By default, NimbleParsec inlines combinators. If you reuse the same combinator (e.g., date) in multiple places within a single defparsec call, it will be compiled multiple times, increasing memory usage and compile times.

    To prevent this, use defcombinatorp/3 to define a reusable, pre-compiled combinator. You can then reference it using parsec(:name) within your main parser definition.

    # Instead of reusing raw combinators:
    # date_then_time = concat(date, time)
    # time_then_date = concat(time, date)
    # defparsec :combinations, choice([date_then_time, time_then_date])
    
    # Use defcombinatorp for reuse:
    defcombinatorp :date, ...
    defcombinatorp :time, ...
    
    date_then_time = concat(parsec(:date), parsec(:time))
    time_then_date = concat(parsec(:time), parsec(:date))
    defparsec :combinations, choice([date_then_time, time_then_date])
  2. Create a parser with defparsec/3

    master

    Use defparsec/3 to compile a parser from a chain of combinators. This macro emits highly optimized binary matching clauses.

    Options:

    • debug: true: Prints the generated compiled clauses to the console. This is useful for inspecting how the combinators are transformed into binary matching code.
    • inline: true: Provides further performance gains by aggressively inlining the generated code.
    defmodule MyParser do
      import NimbleParsec
    
      date = 
        integer(4) 
        |> ignore(string("-")) 
        |> integer(2) 
        |> ignore(string("-")) 
        |> integer(2)
    
      time = 
        integer(2) 
        |> ignore(string(":")) 
        |> integer(2) 
        |> ignore(string(":")) 
        |> integer(2) 
        |> optional(string("Z"))
    
      defparsec :datetime, date |> ignore(string("T")) |> concat(time), debug: true
    end
    
    # Usage:
    MyParser.datetime("2010-04-17T14:12:34Z")
    #=> {:ok, [2010, 4, 17, 14, 12, 34, "Z"], "", %{}, {1, 0}, 20}
  3. Check ahead with lookahead/2

    master

    Checks if a combinator is ahead. If it succeeds, the parser continues as usual. If it fails, it aborts the closest choice/2, repeat/2, etc. If no such operation exists, it errors.

    Note: A lookahead never changes the accumulated output or the context.

    keyword = 
      choice([
        string("if") |> replace(:if),
        string("while") |> replace(:while)
      ])
      |> lookahead(choice([string(" "), eos()]))
  4. Define ASCII characters with ascii_char/2

    master

    Defines a single ASCII codepoint within specified ranges.

    Supported Range Formats:

    • min..max: A standard Elixir range.
    • codepoint: An integer representing a single supported codepoint.
    • {:not, min..max}: Excludes a range of codepoints.
    • {:not, codepoint}: Excludes a specific codepoint.
    defparsec :digit_and_lowercase,
      empty()
      |> ascii_char([?0..?9])
      |> ascii_char([?a..?z])
  5. Define parsers with defparsec/3

    master

    Use defparsec/3 to define a parser and its associated combinator. The parser is a function that accepts a binary and an options keyword list.

    Important: Compilation Constraint defparsec/3 is executed during compilation. You cannot invoke a function defined in the same module because it hasn't been defined yet.

    To solve this, you can:

    1. Use variables to compose the parser.
    2. Define helpers in a separate module and import them.

    Options:

    • :inline - (boolean) Inlines clauses to improve runtime performance at the cost of compilation time and bytecode size.
    • :debug - (boolean) Writes generated clauses to :stderr for debugging.
    • :export_combinator - (boolean) Makes the underlying combinator function public so it can be used via parsec/1 from other modules.
    • :export_metadata - (boolean) Exports metadata necessary to use this parser combinator to generate inputs via generate/1.
    defmodule MyParser do
      import NimbleParsec
    
      # Option 1: Using variables
      date = 
        integer(4)
        |> ignore(string("--"))
        |> integer(2)
        |> ignore(string("--"))
        |> integer(2)
    
      defparsec :date, date
    end
  6. Define integers with integer/2

    master

    Defines an integer combinator. It does not parse the sign and is always base 10.

    Options:

    • count: An integer specifying the exact number of digits.
    • min and max: Keyword list specifying the range of allowed digit lengths (e.g., min: 2, max: 4).

    Efficiency Note: If the difference between min and max is small, using choice([integer(max), integer(max-1), ...]) may be more efficient than using the min/max options.

    defparsec :two_digits_integer, integer(2)
    
    # With min and max
    defparsec :variable_integer, integer(min: 2, max: 4)
  7. Conditional repetition with repeat_while/3

    master

    Use repeat_while/3 to repeat a combinator as long as a provided function returns {:cont, context}. If the function returns {:halt, context}, the repetition stops successfully. The while function receives the rest of the binary, context, position, and offset prepended to its args.

    defmodule MyParser do
      import NimbleParsec
    
      defparsec :string_with_quotes,
                    ascii_char([?" ])
                    |> repeat_while(
                      choice([
                        ~S(") |> string() |> replace(?"),
                        utf8_char([])
                      ]),
                      {:not_quote, []}
                    )
                    |> ascii_char([?" ])
                    |> reduce({List, :to_string, []})
    
      defp not_quote(<<?", _::binary>>, context, _, _), do: {:halt, context}
      defp not_quote(_, context, _, _), do: {:cont, context}
    end
  8. Reduce parser results with reduce/2

    master

    Use reduce/2 to collapse multiple parser results into a single value using a remote or local function. The call argument (a {module, function, args} tuple, {function, args} tuple, or an atom) receives the parser results prepended to the given args.

    defmodule MyParser do
      import NimbleParsec
    
      defparsec :letters_to_reduced_chars,
                    ascii_char([?a..?z])
                    |> ascii_char([?a..?z])
                    |> ascii_char([?a..?z])
                    |> reduce({Enum, :join, ["-"]})
    end
    
    MyParser.letters_to_reduced_chars("abc")
    #=> {:ok, ["97-98-99"], "", %{}, {1, 0}, 3}
  9. Discard output with ignore/2 and replace/2

    master

    ignore/2

    Discards the output of the given combinator.

    replace/2

    Replaces the output of the given combinator with a single value. The replacement value is injected at compile time and must be escapable via Macro.escape/1.

    defmodule MyParser do
      import NimbleParsec
    
      defparsec :replaceable, string("T") |> replace("OTHER") |> integer(2, 2)
    end
    
    MyParser.replaceable("T12")
    #=> {:ok, ["OTHER", 12], "", %{}, {1, 0}, 2}
  10. Repeat combinators with repeat/2 and times/2

    master

    repeat/2

    Allows the given combinator to appear zero or more times.

    Warning: repeat/2 can lead to infinite loops if used with a combinator that can match nothing (like optional/2). It also cannot be used inside choice/2 effectively because it always succeeds, potentially preventing subsequent choices from being attempted. For constrained repetitions, use times/2.

    times/2

    Allows a combinator to appear a specific number of times, or within a range defined by :min and :max options.

    Example:

    defparsec :minimum_lower, times(ascii_char([?a..?z]), min: 2)
    defmodule MyParser do
      import NimbleParsec
    
      defparsec :repeat_lower, repeat(ascii_char([?a..?z]))
    end
    
    MyParser.repeat_lower("abcd")
    #=> {:ok, [?a, ?b, ?c, ?d], "", %{}, {1, 0}, 4}