14 stable releases (4 major)

5.2.0 Nov 26, 2022
5.1.1 Nov 25, 2022
4.1.0 May 7, 2022
4.0.0 Apr 29, 2022
1.2.0 Apr 9, 2022

#2050 in Parser implementations

Download history 75/week @ 2024-07-22 109/week @ 2024-07-29 51/week @ 2024-08-05 49/week @ 2024-08-12 34/week @ 2024-08-19 28/week @ 2024-08-26 30/week @ 2024-09-02 29/week @ 2024-09-09 44/week @ 2024-09-16 84/week @ 2024-09-23 46/week @ 2024-09-30 53/week @ 2024-10-07 50/week @ 2024-10-14 34/week @ 2024-10-21 25/week @ 2024-10-28 69/week @ 2024-11-04

180 downloads per month
Used in toros

GPL-3.0-only

9MB
15K SLoC

C++ 12K SLoC // 0.1% comments Rust 1.5K SLoC // 0.1% comments Happy 730 SLoC Lex 254 SLoC // 0.0% comments

🐉 NixEL

Parser for the Nix Expressions Language.

CI/CD Documentation Coverage Version License

Features

  • ✔️ Fast

    It parses all the files in Nixpkgs in under 25 seconds, single-threaded. [^benchmark-specs]

    It's written in Rust and a little bit of C++, Flex and GNU Bison.

  • ✔️ Correct

    This library is a copy-paste of the original lexer and parser of Nix, with some types adapted for better ergonomy.

    No parser can get closer to the original implementation than this.

  • ✔️ Reliable

    High coverage, battle-tested, and memory-safe[^memory-safe].

  • ✔️ Useful

    It gives you comments, whitespace, starting and end positions, automatic string un-escaping, multiline string indentation handling, a typed API, and everything you need to parse the Nix language!

Usage

You can check out the documentation at docs.rs/nixel.

This is a full usage example:

let input: String = String::from(
    r#"
        # Greet the user
        "Hello, World!"
        # Bye!
    "#,
);

let parsed: nixel::Parsed = nixel::parse(input);

match &*parsed.expression {
    nixel::Expression::String(string) => {
        assert_eq!(
            &string.span,
            &nixel::Span {
                start: nixel::Position { line: 3, column: 9 }.into(),
                end: nixel::Position { line: 3, column: 24 }.into(),
            }
            .into()
        );
        assert_eq!(
            &parsed.trivia_before(&string.span.start)[1],
            &nixel::Trivia::Comment(nixel::TriviaComment {
                content: "# Greet the user".into(),
                span: nixel::Span {
                    start: nixel::Position { line: 2, column: 9 }.into(),
                    end: nixel::Position { line: 2, column: 25 }.into(),
                }
                .into()
            })
        );
        assert_eq!(
            &string.parts[0],
            &nixel::Part::Raw(nixel::PartRaw {
                content: "Hello, World!".into(),
                span: nixel::Span {
                    start: nixel::Position { line: 3, column: 10 }.into(),
                    end: nixel::Position { line: 3, column: 23 }.into(),
                }
                .into()
            })
        );
        assert_eq!(
            &parsed.trivia_after(&string.span.end)[1],
            &nixel::Trivia::Comment(nixel::TriviaComment {
                content: "# Bye!".into(),
                span: nixel::Span {
                    start: nixel::Position { line: 4, column: 9 }.into(),
                    end: nixel::Position { line: 4, column: 15 }.into(),
                }
                .into()
            })
        );
    },
    expression => unreachable!("Expected a String, got: {expression:#?}"),
}

Or from the CLI using Rust's Debug trait:

$ echo '1 + 2' | nix run github:kamadorueda/nixel -- --format=debug

BinaryOperation(
    BinaryOperation {
        left: Integer(
            Integer {
                value: "1",
                span: Span {
                    start: Position {
                        line: 1,
                        column: 1,
                    },
                    end: Position {
                        line: 1,
                        column: 2,
                    },
                },
            },
        ),
        operator: Addition,
        right: Integer(
            Integer {
                value: "2",
                span: Span {
                    start: Position {
                        line: 1,
                        column: 5,
                    },
                    end: Position {
                        line: 1,
                        column: 6,
                    },
                },
            },
        ),
    },
)

Or from the CLI using JSON format:

$ echo '1 + 2' | nix run github:kamadorueda/nixel -- --format=json

{
  "BinaryOperation": {
    "left": {
      "Integer": {
        "value": "1",
        "span": {
          "start": {
            "line": 1,
            "column": 1
          },
          "end": {
            "line": 1,
            "column": 2
          }
        }
      }
    },
    "operator": "Addition",
    "right": {
      "Integer": {
        "value": "2",
        "span": {
          "start": {
            "line": 1,
            "column": 5
          },
          "end": {
            "line": 1,
            "column": 6
          }
        }
      }
    }
  }
}

You can check out more examples in the tests folder.

Alternatives

License

Please read LICENSE.md.

Footnotes

[^benchmark-specs]: Running on a machine with:

- CPU: 4 physical, 4 logical, 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
- MHz: from 400 to 4700 MHz
- BogoMips: 5606.40
- Cache L3: 12 MiB

The following command takes around 1 minute:

```bash
$ nix build --system x86_64-linux
$ time find /data/nixpkgs -type f -name '*.nix' \
  -exec ./result/bin/nixel --format=none {} \;

real  0m24.293s
user  0m15.066s
sys   0m8.955s
```

[^memory-safe]: Tested under real-life workloads using Valgrind, and by running an infinite loop of parsing cycles over Nixpkgs :).

```bash
$ nix build --system x86_64-linux
$ valgrind ./result/bin/nixel $file

  LEAK SUMMARY:
    definitely lost: 0 bytes in 0 blocks
    indirectly lost: 0 bytes in 0 blocks
      possibly lost: 0 bytes in 0 blocks
         suppressed: 0 bytes in 0 blocks
```

Dependencies