build(rust): vendor rnix parser crate

This commit was merged in pull request #57.
This commit is contained in:
alexion committed 2026-09-16 15:51:43 -04:00
1 parent cbdf482422
commit 82ad65a3a4
492 files changed
+67785 -1

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"fcdcfcbe69ba219427d7e4f0435343775127b3dc292fac94a5729ec7d5e5e804",".envrc":"1d26c74fd25a4c3dbb09e029fc609588da499fd4af2a41c88f6316c7f8c54cf1",".github/ISSUE_TEMPLATE/bug_report.md":"e029cf740ebfff7edd8ac7a71914cdd06d0dbb5d2c6a82c739aaf0f3d9fe7e90",".github/ISSUE_TEMPLATE/feature_request.md":"4589c7cf6d0d04b692ea0a4d99187488d9631bf1c9d4712aa41478c0907dea3f",".github/ISSUE_TEMPLATE/question.md":"e555a3b9fe9e67aeca3245a5a0162a9fe08516603989a80112b001a1c91b7b67",".github/PULL_REQUEST_TEMPLATE.md":"2eb53504c7972ee90a450a53f651694b73a0c1c9d1f68d1e09b9650e682b96e2",".github/dependabot.yml":"883b95e3be554fbadd92b315e4a8c6ea6ca688b64924e22dd61222b07f683507",".github/workflows/ci.yml":"565bb7e946d21cb3d1141d87422df88536ca5c84de0f9bbc7c79c623583e99d9","CHANGELOG.md":"e98de826b40a296bd75050d744b71a1a4aa6944e29f36ef1c2d38c3788152dd0","Cargo.lock":"ab7e6cdb7273dffd1189bdccea6721dbbf0cf4e6f16ac9807c3bd299efab4492","Cargo.toml":"1710fd19c321d58563c6a375e6c4fc26c94511049bcfb3024e426d16eb4df83a","Cargo.toml.orig":"663ac3c05de129104746eeaeef3b6a6821eb6af5b5fff4834cb53eb317829ac5","LICENSE":"6c54a9750f1e6da4ad6618c569f2541b1df31921e2b30651886fc8893dfc3e76","MIGRATING.md":"baff376c0509c119628e318ffedeed21703f8ea32db8de743014643b7e2104c4","README.md":"4a09818de05e456506d5ee150a73b8ff92aaf6897ec8def03d3c9688c82a91a2","benches/all-packages.nix":"935f239a576caa554e546072772039f6efcbde2f2e6568733b8857fe476c5604","benches/all-packages.rs":"e64ed71c76a13eba2c2444062423122f5ad8ced00ab6de1951e19e1806313926","default.nix":"d84fab688ba7f236a7ad9db1eedfa8dd68bfec1271c5aea81cc0ecdc84d3ea84","examples/dump-ast.rs":"b3759df9673065fc661258f32a50aaae25b5573faef986dc47658ae3f6c94cc5","examples/error-report.rs":"ab3f5357d3ff3816b2355ad526fcfc9a33a8c5dc5dd434cfcd82a4b951643e52","examples/from-stdin.rs":"77726afba4b0c79b46e7efd2a2dc940bedb0bc9e30dca37d7443b33e306433d1","examples/list-fns.rs":"01d3f433cbeebfedc1c5d8106db0f1e9965b587c30244e66b504b98828e6d433","examples/preserve.rs":"971a77999a84bc11b9a3afe179bd8e51f4de475009e776c42890ac02c2eaf030","examples/test-nixpkgs.rs":"0924d62302f5486a87d26597af6f78497a6ed2439452efe14b5262d3644a21be","flake.lock":"a71d5cc0ebeed8335099205ad2e6eece037f39faf609de2b2afd83c1d0b1c0fc","flake.nix":"bc94dbe872df56b1b16289dcfdd5db0a627fd6165331c2d11b5bae8b2481da57","rustfmt.toml":"f74204a6f92aa7422a16ecb2ffe2d5bae0f123b778d08b5db1a398a3c9ca4306","shell.nix":"6fd207b4354fc12c29f603ba106d4bc5a6becbc0f13de4f90846ee10b2054bb1","src/ast.rs":"0307dd2b362ce111a4b730318c7df75f1d62534acf7a4a295972ae5369e02141","src/ast/expr_ext.rs":"53afc86db8b404aa7fb1f2a7c47a925cb83bea95cd3b5e52b44963439892be3d","src/ast/interpol.rs":"7d9c8baa98fb47d691e55a1106f5e457c6c786dbc00d27e015acf6f67c3248e1","src/ast/nodes.rs":"950d9e7c0fe9320e4e5ce19aaa1c06aeab6fc3236199a7ef9f395180ae3eb423","src/ast/operators.rs":"51ae735d38623d0490efd21d7060806aadbe1f91589f5cde922c1b50a0941a98","src/ast/path_util.rs":"8dfe591cb762e630bc0271c2a1875be0b6c3b35876af118cfd4edc055e1cf34c","src/ast/str_util.rs":"448e5e2805552e0a386569e8f497ef10802c5b7d45360fa345b0dd1b36b82d3f","src/ast/tokens.rs":"7d6aec7ed91930cae7d472df800e6eccff4afe855084928e22f0460d1a0017a3","src/kinds.rs":"3a06fd766de635066eccabcca90f5698458d7bf868a9fb12410f90a8766a6d0e","src/lib.rs":"dd33d6ab292d2c1c2f303392fc5a34ebb96106b61b6a3ea32b97fcc4e6f007ca","src/macros.rs":"6f222ffebe7d2b7ba9d653e050ce680cb34f5ccad5046b1a74ca000e27cf10ed","src/parser.rs":"facbcf26a37d0702da21aab5ec65bb5e69e8b4fd70fd8b1fd33ad54610c5b56e","src/tests.rs":"4bbdf0c133806aab2404e6449af3c4669b4c9632206b10b60db2e2f4a2c05f96","src/token_set.rs":"5b2b9f7a8dc04a0b9334bf19b38bf51dc7237eea3c1d6d07df06f3e94b948079","src/tokenizer.rs":"90e0db5df66f75a6098277f695f79f98098b099534d23d9e8ef8547d5d690712","test_data/parser/error/dollar_ident_attrpath_issue174.expect":"01262a025ed92dadfbcc3ec6e4fa70020a2d79d42e85c330cff76b0e35eee4a9","test_data/parser/error/dollar_ident_attrpath_issue174.nix":"213745923795602e58733d53a142ccbf7a46f15820a3e8dd351fe03c387792dd","test_data/parser/error/error.expect":"22f46b02e4c35c07a42f68c9096d2c97e520e4411da6beaf5146f128210dde87","test_data/parser/error/error.nix":"ab96e082ceb9a8fd295b1b9770d1a092fecc957f9dfa9393a51572db005f6510","test_data/parser/error/extra_comma.expect":"83cea8dfcdccb17eebda361f9d858082012b2090f6ab50a9f8b7ec05a65e6140","test_data/parser/error/extra_comma.nix":"a67d8cf479d324f353bdbaff9e94772d6acc74807c07959fa0b51e83f431b258","test_data/parser/error/formals_double_bind.expect":"8659ab05254da887bebff84e60c9ddf9aead1fc10d12594455e83826de39db1b","test_data/parser/error/formals_double_bind.nix":"ad473bbab713d5fe1c2cbddebe176962d1fab8a03e76a348347f7587c7ab27d5","test_data/parser/error/inherit_from_late.expect":"fbda47c9f72a1b48f9d312c33f65dc6b054258a717d9c5fa4bac5cb158ef35d7","test_data/parser/error/inherit_from_late.niLine truncated
+7
View File
@@ -0,0 +1,7 @@
{
"git": {
"sha1": "0472081214c24b1ab4d34f7bf544284ed4e45ad3",
"dirty": true
},
"path_in_vcs": ""
}
@@ -0,0 +1,36 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug'
assignees: ''
---
<!--
Feel free to remove paragraphs that don't apply to your bugreport.
-->
### Describe the bug
<!-- A clear and concise description of what the bug is.-->
### Code Snippet to reproduce
<!--
If applicable, a Nix expression that demonstrates the problem.
-->
```nix
```
### Expected behavior
<!-- A clear and concise description of what you expected to happen.-->
### Additional context
<!--
Anything else you consider relevant such as `rnix` version (mandatory), related issues etc.
-->
@@ -0,0 +1,29 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
<!--
Feel free to remove paragraphs that don't apply to your request.
-->
### Description
<!--
Describe the feature / a possible solution you'd like to see.
-->
### Considered alternatives
<!--
A clear and concise description of any alternative solutions or features you've considered.
-->
### Additional context
<!--
Add any context such as screenshots, related issues or whatever you consider relevant here.
-->
@@ -0,0 +1,7 @@
---
name: Question
about: Ask a general question about the project
title: ''
labels: 'question'
assignees: ''
---
@@ -0,0 +1,25 @@
<!--
Feel free to remove paragraphs that don't apply to your PR.
-->
### Summary & Motivation
<!--
Please summarize the changes you've made and the motivation behind it.
-->
### Backwards-incompatible changes
<!--
If applicable, please summarize which changes of your PR are backwards-incompatible
to the latest release.
-->
### Further context
<!--
If applicable, please give further context such as related issues / PRs,
an explanation of the problem you're aiming to solve or anything else you also
consider relevant.
-->
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+64
View File
@@ -0,0 +1,64 @@
name: ci
on:
pull_request:
push:
branches:
- master
jobs:
test:
name: test
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Dependencies
uses: Swatinem/rust-cache@v2
- name: Compile
run: cargo test --no-run --locked
- name: Test
run: cargo test -- --nocapture
rustfmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
clippy:
name: clippy
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Run Clippy
run: cargo clippy
+113
View File
@@ -0,0 +1,113 @@
###############################################################################
# @generated
# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To
# regenerate this file, run the following:
#
# bazel run @@//third_party:vendor_crates
###############################################################################
load("@rules_rust//cargo:defs.bzl", "cargo_toml_env_vars")
load("@rules_rust//rust:defs.bzl", "rust_library")
package(default_visibility = ["//visibility:public"])
cargo_toml_env_vars(
name = "cargo_toml_env_vars",
src = "Cargo.toml",
)
rust_library(
name = "rnix",
srcs = glob(
include = ["**/*.rs"],
allow_empty = False,
),
compile_data = glob(
include = ["**"],
allow_empty = True,
exclude = [
"**/* *",
".tmp_git_root/**/*",
"BUILD",
"BUILD.bazel",
"WORKSPACE",
"WORKSPACE.bazel",
],
),
crate_root = "src/lib.rs",
edition = "2021",
rustc_env_files = [
":cargo_toml_env_vars",
],
rustc_flags = [
"--cap-lints=allow",
],
tags = [
"cargo-bazel",
"crate-name=rnix",
"manual",
"noclippy",
"norustfmt",
],
target_compatible_with = select({
"@rules_rust//rust/platform:aarch64-apple-darwin": [],
"@rules_rust//rust/platform:aarch64-apple-ios": [],
"@rules_rust//rust/platform:aarch64-apple-ios-macabi": [],
"@rules_rust//rust/platform:aarch64-apple-ios-sim": [],
"@rules_rust//rust/platform:aarch64-linux-android": [],
"@rules_rust//rust/platform:aarch64-pc-windows-msvc": [],
"@rules_rust//rust/platform:aarch64-unknown-fuchsia": [],
"@rules_rust//rust/platform:aarch64-unknown-linux-gnu": [],
"@rules_rust//rust/platform:aarch64-unknown-nixos-gnu": [],
"@rules_rust//rust/platform:aarch64-unknown-none": [],
"@rules_rust//rust/platform:aarch64-unknown-nto-qnx710": [],
"@rules_rust//rust/platform:aarch64-unknown-uefi": [],
"@rules_rust//rust/platform:arm-unknown-linux-gnueabi": [],
"@rules_rust//rust/platform:arm-unknown-linux-musleabi": [],
"@rules_rust//rust/platform:armv7-linux-androideabi": [],
"@rules_rust//rust/platform:armv7-unknown-linux-gnueabi": [],
"@rules_rust//rust/platform:i686-apple-darwin": [],
"@rules_rust//rust/platform:i686-linux-android": [],
"@rules_rust//rust/platform:i686-pc-windows-msvc": [],
"@rules_rust//rust/platform:i686-unknown-freebsd": [],
"@rules_rust//rust/platform:i686-unknown-linux-gnu": [],
"@rules_rust//rust/platform:loongarch64-unknown-linux-gnu": [],
"@rules_rust//rust/platform:mips-unknown-linux-gnu": [],
"@rules_rust//rust/platform:powerpc-unknown-linux-gnu": [],
"@rules_rust//rust/platform:riscv32imac-unknown-none-elf": [],
"@rules_rust//rust/platform:riscv32imc-unknown-none-elf": [],
"@rules_rust//rust/platform:riscv64gc-unknown-linux-gnu": [],
"@rules_rust//rust/platform:riscv64gc-unknown-none-elf": [],
"@rules_rust//rust/platform:s390x-unknown-linux-gnu": [],
"@rules_rust//rust/platform:sparc64-unknown-linux-gnu": [],
"@rules_rust//rust/platform:sparc64-unknown-netbsd": [],
"@rules_rust//rust/platform:sparc64-unknown-openbsd": [],
"@rules_rust//rust/platform:thumbv6m-none-eabi": [],
"@rules_rust//rust/platform:thumbv7em-none-eabi": [],
"@rules_rust//rust/platform:thumbv7em-none-eabihf": [],
"@rules_rust//rust/platform:thumbv7m-none-eabi": [],
"@rules_rust//rust/platform:thumbv8m.main-none-eabi": [],
"@rules_rust//rust/platform:thumbv8m.main-none-eabihf": [],
"@rules_rust//rust/platform:wasm32-unknown-emscripten": [],
"@rules_rust//rust/platform:wasm32-unknown-unknown": [],
"@rules_rust//rust/platform:wasm32-wasip1": [],
"@rules_rust//rust/platform:wasm32-wasip1-threads": [],
"@rules_rust//rust/platform:wasm32-wasip2": [],
"@rules_rust//rust/platform:x86_64-apple-darwin": [],
"@rules_rust//rust/platform:x86_64-apple-ios": [],
"@rules_rust//rust/platform:x86_64-apple-ios-macabi": [],
"@rules_rust//rust/platform:x86_64-linux-android": [],
"@rules_rust//rust/platform:x86_64-pc-windows-msvc": [],
"@rules_rust//rust/platform:x86_64-unknown-freebsd": [],
"@rules_rust//rust/platform:x86_64-unknown-fuchsia": [],
"@rules_rust//rust/platform:x86_64-unknown-linux-gnu": [],
"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu": [],
"@rules_rust//rust/platform:x86_64-unknown-none": [],
"@rules_rust//rust/platform:x86_64-unknown-uefi": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
version = "0.14.0",
deps = [
"//third_party/crates/rowan-0.16.1:rowan",
],
)
+190
View File
@@ -0,0 +1,190 @@
# Changelog
All notable changes between releases will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [v0.14.0] - 2026-02-16
### Fixed
* Fix absolute path interpolation starting with `/${` failing to parse
([#182](https://github.com/nix-community/rnix-parser/issues/182),
from [@imxyy1soope1](https://github.com/imxyy1soope1)).
### Breaking Changes
* **`__curPos` is now a distinct node type.** `__curPos` is a Nix keyword that evaluates to an
attribute set containing the file, line, and column where it appears.
[Lix documents this as a keyword.](https://docs.lix.systems/manual/lix/nightly/language/constructs.html#keywords-__curPos)
It was previously represented as `NODE_IDENT`, but is now parsed as `NODE_CUR_POS` with a
corresponding `TOKEN_CUR_POS` token kind (from [@3timeslazy](https://github.com/3timeslazy)).
## [v0.13.0] - 2026-01-29
### Breaking Changes
* **Path types are now distinguished at the type level.** `TOKEN_PATH` and `NODE_PATH` have been
replaced with specific variants for each path type:
- `TOKEN_PATH_ABS` / `NODE_PATH_ABS` - absolute paths (`/foo/bar`)
- `TOKEN_PATH_REL` / `NODE_PATH_REL` - relative paths (`./foo`, `../bar`, `foo/bar`)
- `TOKEN_PATH_HOME` / `NODE_PATH_HOME` - home-relative paths (`~/foo`)
- `TOKEN_PATH_SEARCH` / `NODE_PATH_SEARCH` - search paths (`<nixpkgs>`)
The `ast::Expr` enum now has `PathAbs`, `PathRel`, `PathHome`, and `PathSearch` variants
instead of a single `Path` variant. See `MIGRATING.md` for upgrade guidance.
* **Double slashes in paths are now syntax errors.** Paths like `foo//bar` now produce
`TOKEN_ERROR` instead of being parsed.
* **The `parser` and `tokenizer` modules are now private.** Use `rnix::Root::parse()` and
`rnix::tokenize()` instead.
### Added
* Implement `FromStr` for `SyntaxKind` (from [@abhillman](https://github.com/abhillman)).
* Add `MIGRATING.md` with upgrade guidance for breaking changes.
* Add `nix develop .#fuzz` devShell for running cargo-fuzz with nightly Rust.
### Fixed
* Fix leading zero number parsing to match Nix behavior (from [@hsjobeki](https://github.com/hsjobeki)).
### Changed
* Update `rowan` to 0.16.
* Update `criterion` to 0.8.
* Update `expect-test` to 1.5.1.
* Add Clippy to CI and resolve all warnings.
## [v0.12.0] - 2025-01-09
* Add support for pipe operators
## [v0.11.0] - 2022-11-11
* removes the `types` module and replaces it with `ast`
* the `ast` modules has better types and sum types
* removes `ParsedType` in favor of `Expr`
* removes the `value` module. methods on `Value` go on the individual types. For example, string related methods go on `ast::Str`
* `parse` changed to `Root::parse`
* `NODE_OR_DEFAULT` is removed
* `NODE_KEY` -> `NODE_ATTRPATH`, `NODE_KEY_VALUE` -> `NODE_ATTRPATH_VALUE`
* `BinOp::IsSet` is removed since it's actually not a normal binary operator.
* `NODE_SELECT` is flattened in favor of `NODE_ATTRPATH` which consists of multiple Attrs, including `a.b.c` and `a.b.c or expr`
* `a or b` is Apply, not "OrDefault", which matches the result of Nix. Fixes #23
* `${` is considered invalid at Expr places now, which matches the result of Nix.
* remove `TOKEN_DYNAMIC_START` and `TOKEN_DYNAMIC_END`
* rowan 15.0
* various other parsing fixes
## [v0.10.2] - 2022-06-14
* Correctly parse every possible case of path-interpolation in Nix code ([#85](https://github.com/nix-community/rnix-parser/issues/85)).
(from [@ncfavier](https://github.com/ncfavier)).
## [v0.10.1] - 2021-12-06
### Fixed
* Trivia tokens (i.e. ` `, `\r`, `\n` etc.) are no longer misplaced around path-expressions.
This is a regression from `v0.10.0` which would've broken `nixpkgs-fmt`.
(from [@Ma27](https://github.com/Ma27)).
## [v0.10.0] - 2021-11-30
### Added
* Support for the new path-interpolation syntax from Nix 2.4 was added. The parser
now correctly detects
```nix
./foo/${bar}
```
as valid code and parses it down to
```
NODE_ROOT 0..13 {
NODE_PATH_WITH_INTERPOL 0..12 {
TOKEN_PATH("./foo/") 0..6
NODE_STRING_INTERPOL 6..12 {
TOKEN_INTERPOL_START("${") 6..8
NODE_IDENT 8..11 {
TOKEN_IDENT("bar") 8..11
}
TOKEN_INTERPOL_END("}") 11..12
}
}
TOKEN_WHITESPACE("\n") 12..13
}
```
Paths without interpolation are still within `NODE_LITERAL` for backwards-compatibility.
(from [@Ma27](https://github.com/Ma27)).
* `rnix::types::ParsedTypeError` now implements the `Debug`, `Copy`, `Clone`, `fmt::Display` traits (from [@NerdyPepper](https://github.com/NerdyPepper)).
* During development, the parser can be tested like this:
```
$ cargo run --example from-stdin <<< 'builtins.map (x: x * x) [ 1 2 3 ]'
```
(from [@efx](https://github.com/efx)).
### Changed
* A few more examples of invalid `inherit`-expressions are correctly marked as such (from [@oberblastmeister](https://github.com/oberblastmeister)).
* `rnix::types::BinOp::operator()` now returns `Option<BinOpKind>` rather than unwrapping the
value (from [@NerdyPepper](https://github.com/NerdyPepper)).
If your code worked fine before, it will be sufficient to add a `.unwrap()` since this was
what happened before within `::operator()`.
* Duplicated arguments in the argument-pattern syntax are now marked as parser error. E.g.
```nix
{ a, a }: a
```
now produces an error (from [@Ma27](https://github.com/Ma27)).
* Floats without trailing zeroes (e.g. `.5`) are no longer marked as syntax error (from [@Ma27](https://github.com/Ma27)).
## [v0.9.1] - 2021-09-23
### Changed
* Fixed a memory leak while parsing `inherit`-expressions with invalid syntax (from [@Ma27](https://github.com/Ma27/)).
[Unreleased]: https://github.com/nix-community/rnix-parser/compare/v0.14.0...master
[v0.14.0]: https://github.com/nix-community/rnix-parser/compare/v0.13.0...v0.14.0
[v0.13.0]: https://github.com/nix-community/rnix-parser/compare/v0.12.0...v0.13.0
[v0.12.0]: https://github.com/nix-community/rnix-parser/compare/v0.11.0...v0.12.0
[v0.11.0]: https://github.com/nix-community/rnix-parser/compare/v0.10.2...v0.11.0
[v0.10.2]: https://github.com/nix-community/rnix-parser/compare/v0.10.1...v0.10.2
[v0.10.1]: https://github.com/nix-community/rnix-parser/compare/v0.10.0...v0.10.1
[v0.10.0]: https://github.com/nix-community/rnix-parser/compare/v0.9.1...v0.10.0
[v0.9.1]: https://github.com/nix-community/rnix-parser/compare/v0.9.0...v0.9.1
+660
View File
@@ -0,0 +1,660 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "countme"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
[[package]]
name = "criterion"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"itertools",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
"serde",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "dissimilar"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "expect-test"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0"
dependencies = [
"dissimilar",
"once_cell",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rnix"
version = "0.14.0"
dependencies = [
"criterion",
"expect-test",
"rowan",
]
[[package]]
name = "rowan"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
dependencies = [
"countme",
"hashbrown",
"rustc-hash",
"text-size",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "text-size"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zerocopy"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
+68
View File
@@ -0,0 +1,68 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "rnix"
version = "0.14.0"
authors = ["jD91mZM2 <me@krake.one>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "A Nix parser written in Rust"
readme = "README.md"
license = "MIT"
repository = "https://github.com/nix-community/rnix-parser"
[lib]
name = "rnix"
path = "src/lib.rs"
[[example]]
name = "dump-ast"
path = "examples/dump-ast.rs"
[[example]]
name = "error-report"
path = "examples/error-report.rs"
[[example]]
name = "from-stdin"
path = "examples/from-stdin.rs"
[[example]]
name = "list-fns"
path = "examples/list-fns.rs"
[[example]]
name = "preserve"
path = "examples/preserve.rs"
[[example]]
name = "test-nixpkgs"
path = "examples/test-nixpkgs.rs"
[[bench]]
name = "all-packages"
path = "benches/all-packages.rs"
harness = false
[dependencies.rowan]
version = "0.16"
[dev-dependencies.criterion]
version = "0.8"
[dev-dependencies.expect-test]
version = "1.5.1"
+20
View File
@@ -0,0 +1,20 @@
[package]
authors = ["jD91mZM2 <me@krake.one>"]
description = "A Nix parser written in Rust"
edition = "2021"
license = "MIT"
name = "rnix"
readme = "README.md"
repository = "https://github.com/nix-community/rnix-parser"
version = "0.14.0"
[[bench]]
harness = false
name = "all-packages"
[dependencies]
rowan = "0.16"
[dev-dependencies]
criterion = "0.8"
expect-test = "1.5.1"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 jD91mZM2
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+104
View File
@@ -0,0 +1,104 @@
# Migration Guide
## Migrating to 0.13.0
### Path types are now distinguished at the type level
Previously, all Nix paths were parsed into a single `TOKEN_PATH` / `NODE_PATH` type, requiring runtime string inspection to determine the path variant. Now, paths are distinguished at the type level:
| Path Type | Example | Token | Node |
|-----------|---------|-------|------|
| Absolute | `/foo/bar` | `TOKEN_PATH_ABS` | `NODE_PATH_ABS` |
| Relative | `./foo`, `../bar`, `foo/bar` | `TOKEN_PATH_REL` | `NODE_PATH_REL` |
| Home-relative | `~/foo` | `TOKEN_PATH_HOME` | `NODE_PATH_HOME` |
| Search | `<nixpkgs>` | `TOKEN_PATH_SEARCH` | `NODE_PATH_SEARCH` |
#### Updating `SyntaxKind` matches
```rust
// Before:
match kind {
SyntaxKind::TOKEN_PATH => { /* ... */ }
SyntaxKind::NODE_PATH => { /* ... */ }
}
// After:
match kind {
SyntaxKind::TOKEN_PATH_ABS
| SyntaxKind::TOKEN_PATH_REL
| SyntaxKind::TOKEN_PATH_HOME
| SyntaxKind::TOKEN_PATH_SEARCH => { /* ... */ }
SyntaxKind::NODE_PATH_ABS
| SyntaxKind::NODE_PATH_REL
| SyntaxKind::NODE_PATH_HOME
| SyntaxKind::NODE_PATH_SEARCH => { /* ... */ }
}
```
#### Updating `Expr` matches
The `Expr` enum no longer has a `Path` variant. Instead, there are four separate variants:
```rust
// Before:
match expr {
ast::Expr::Path(p) => {
// Had to inspect the string to determine path type
}
// ...
}
// After:
match expr {
ast::Expr::PathAbs(p) => { /* absolute path like /foo/bar */ }
ast::Expr::PathRel(p) => { /* relative path like ./foo */ }
ast::Expr::PathHome(p) => { /* home path like ~/foo */ }
ast::Expr::PathSearch(p) => { /* search path like <nixpkgs> */ }
// ...
}
```
#### Using the `Path` enum
If you need to handle all path types uniformly, use the `ast::Path` enum:
```rust
use rnix::ast::{AstNode, Path};
// Cast any path node to the Path enum
if let Some(path) = Path::cast(node) {
match path {
Path::PathAbs(p) => { /* ... */ }
Path::PathRel(p) => { /* ... */ }
Path::PathHome(p) => { /* ... */ }
Path::PathSearch(p) => { /* ... */ }
}
// Or use helper methods
if path.is_search() {
// Search paths don't support interpolation
}
// Get parts from any path type
let parts = path.parts();
}
```
#### `Path::parts()` return type change
The `parts()` method now returns `Vec<InterpolPart<PathContent>>` instead of `impl Iterator<...>`:
```rust
// Before:
let mut parts = path.parts();
let first = parts.next();
// After:
let parts = path.parts();
let first = parts.first();
```
### Double slashes in paths are now syntax errors
Paths containing `//` (e.g., `foo//bar`) are now tokenized as `TOKEN_ERROR`. This matches Nix's behavior where such paths are invalid.
+88
View File
@@ -0,0 +1,88 @@
# rnix-parser [![Crates.io](https://img.shields.io/crates/v/rnix.svg)](http://crates.io/crates/rnix) [![Chat on Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#rnix-lsp:matrix.org)
rnix is a parser for the [Nix language](https://nixos.org/nix/) written in Rust.
This can be used to manipulate the Nix AST and can for example be used for:
- Interactively render Nix on a GUI
- Formatting Nix code
- Rename identifiers
and a lot more!
rnix nowadays uses [@matklad](https://github.com/matklad)'s
[rowan](https://crates.io/crates/rowan) crate to ensure:
- all span information is preserved, meaning you can use the AST to for
example apply highlighting
- printing out the AST prints out 100% the original code. This is not an
over-exaggeration, even completely invalid nix code such as this README can
be intact after a parsing session (though the AST will mark errnous nodes)
- easy ways to walk the tree without resorting to recursion
## Demo
Examples can be found in the `examples/` directory.
You may also want to see
[nix-explorer](https://gitlab.com/jD91mZM2/nix-explorer): An example
that highlights AST nodes in Nix code. This demonstrates how
whitespaces and commands are preserved.
## Hacking
Tests can be run with `cargo test`.
In order to update all `.expect`-files to the currently expected results,
you may run `UPDATE_TESTS=1 cargo test`.
You can parse Nix expressions from standard input using the `from-stdin` example.
To try that, run the following in your shell:
```sh
echo "[hello nix]" | cargo run --quiet --example from-stdin
```
## Release Checklist
* Ensure that all PRs that were scheduled for the release are merged (or optionally move
them to another milestone).
* Close the milestone for the release (if any).
* Run `cargo test` on `master` (or the branch to release from) with all changes being pulled in.
* Apply the following patch to [nixpkgs-fmt](https://github.com/nix-community/nixpkgs-fmt):
```diff
diff --git a/Cargo.toml b/Cargo.toml
index 0891350..edad471 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,9 @@ repository = "https://github.com/nix-community/nixpkgs-fmt"
[workspace]
members = [ "./wasm" ]
+[patch.crates-io]
+rnix = { path = "/home/ma27/Projects/rnix-parser" }
+
[dependencies]
rnix = "0.9.0"
smol_str = "0.1.17"
```
and run `cargo test`.
While it's planned to add [fuzzing to `rnix-parser` as well](https://github.com/nix-community/rnix-parser/issues/32),
`nixpkgs-fmt` has a decent test-suite that would've helped to catch regressions in the past.
__Note:__ API changes are OK (and fixes should be contributed to `nixpkgs-fmt`), behavioral changes
are not unless explicitly discussed before.
* Update the [CHANGELOG.md](https://github.com/nix-community/rnix-parser/blob/master/CHANGELOG.md).
* Bump the version number in [Cargo.toml](https://github.com/nix-community/rnix-parser/blob/master/Cargo.toml) & re-run `cargo build` to refresh the lockfile.
* Commit & run `nix build`.
* Tag the release and push everything.
* As soon as the CI has completed, run `cargo publish`.
# RIP jd91mzm2
Sadly, the original author of this project, [@jD91mZM2 has passed
away](https://www.redox-os.org/news/open-source-mental-health/). His online
presence was anonymous and what we have left is his code. This is but one of
his many repos that he contributed to.
File diff suppressed because it is too large. Load diff
+15
View File
@@ -0,0 +1,15 @@
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
fn all_packages(c: &mut Criterion) {
let input = include_str!("all-packages.nix");
let mut group = c.benchmark_group("all-packages");
group.throughput(Throughput::Bytes(input.len() as u64));
group.sample_size(30);
group.bench_with_input("all-packages", input, move |b, input| {
b.iter(|| rnix::Root::parse(input))
});
group.finish();
}
criterion_group!(benches, all_packages);
criterion_main!(benches);
+9
View File
@@ -0,0 +1,9 @@
let
flake-compat = builtins.fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/12c64ca55c1014cdc1b16ed5a804aa8576601ff2.tar.gz";
sha256 = "0jm6nzb83wa6ai17ly9fzpqc40wg1viib8klq8lby54agpl213w5";
};
in
(import flake-compat {
src = ./.;
}).defaultNix.default
+24
View File
@@ -0,0 +1,24 @@
use std::{env, fs};
fn main() {
let mut iter = env::args().skip(1).peekable();
if iter.peek().is_none() {
eprintln!("Usage: dump-ast <file>");
return;
}
for file in iter {
let content = match fs::read_to_string(file) {
Ok(content) => content,
Err(err) => {
eprintln!("error reading file: {}", err);
return;
}
};
let parse = rnix::Root::parse(&content);
for error in parse.errors() {
println!("error: {}", error);
}
println!("{:#?}", parse.tree());
}
}
+67
View File
@@ -0,0 +1,67 @@
use rnix::ParseError;
use std::{env, fs};
fn main() {
let file = match env::args().nth(1) {
Some(file) => file,
None => {
eprintln!("Usage: error-report <file>");
return;
}
};
let content = match fs::read_to_string(&file) {
Ok(content) => content,
Err(err) => {
eprintln!("error reading file: {}", err);
return;
}
};
let ast = rnix::Root::parse(&content);
for error in ast.errors() {
let range = match error {
ParseError::Unexpected(range) => range,
ParseError::UnexpectedExtra(range) => range,
ParseError::UnexpectedWanted(_, range, _) => range,
ParseError::UnexpectedDoubleBind(range) => range,
ParseError::DuplicatedArgs(range, _) => range,
err => {
eprintln!("error: {}", err);
continue;
}
};
eprintln!("----- ERROR -----");
eprintln!("{}", error);
let start = usize::from(range.start());
let start_row = content[..start].lines().count() - 1;
let start_line = content[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
let start_col = content[start_line..start].chars().count();
let end = usize::from(range.end());
let end_row = content[..end].lines().count() - 1;
let end_line = content[..end].rfind('\n').map(|i| i + 1).unwrap_or(0);
let end_col = content[end_line..end].chars().count();
let mut line_len = 1;
let mut line = end_row;
while line >= 10 {
line /= 10;
line_len += 1;
}
let i = start_row.saturating_sub(1);
for (i, line) in content.lines().enumerate().skip(i).take(end_row - i + 1) {
println!("{:line_len$} {}", i + 1, line, line_len = line_len);
if i >= start_row && i <= end_row {
print!("{:line_len$} ", "", line_len = line_len);
let mut end_col = if i == end_row { end_col } else { line.chars().count() };
if i == start_row {
print!("{:indent$}", "", indent = start_col);
end_col -= start_col;
}
for _ in 0..end_col {
print!("^");
}
println!();
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
use std::{io, io::Read};
fn main() {
let mut content = String::new();
io::stdin().read_to_string(&mut content).expect("could not read nix from stdin");
let ast = rnix::Root::parse(&content);
for error in ast.errors() {
println!("error: {}", error);
}
println!("{:#?}", ast.tree());
}
+95
View File
@@ -0,0 +1,95 @@
use std::{env, error::Error, fs};
use rnix::{
ast::{self, AstToken, HasEntry},
match_ast, NodeOrToken, SyntaxNode,
};
use rowan::ast::AstNode;
macro_rules! single_match {
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? => $captured:expr) => {
match $expression {
$( $pattern )|+ $( if $guard )? => Some($captured),
_ => None
}
};
}
fn main() -> Result<(), Box<dyn Error>> {
let file = match env::args().nth(1) {
Some(file) => file,
None => {
eprintln!("Usage: list-fns <file>");
return Ok(());
}
};
let content = fs::read_to_string(&file)?;
let ast = rnix::Root::parse(&content).ok()?;
let expr = ast.expr().unwrap();
let set = match expr {
ast::Expr::AttrSet(set) => set,
_ => return Err("root isn't a set".into()),
};
for entry in set.entries() {
if let ast::Entry::AttrpathValue(attrpath_value) = entry {
if let Some(ast::Expr::Lambda(lambda)) = attrpath_value.value() {
let attrpath = attrpath_value.attrpath().unwrap();
let ident = attrpath.attrs().last().and_then(|attr| match attr {
ast::Attr::Ident(ident) => Some(ident),
_ => None,
});
let s = ident.as_ref().map_or_else(
|| "error".to_string(),
|ident| ident.ident_token().unwrap().text().to_string(),
);
println!("Function name: {}", s);
{
let comments = comments_before(attrpath_value.syntax());
if !comments.is_empty() {
println!("--> Doc: {comments}");
}
}
let mut value = Some(lambda);
while let Some(lambda) = value {
let s = lambda
.param()
.as_ref()
.map_or_else(|| "error".to_string(), |param| param.to_string());
println!("-> Param: {}", s);
{
let comments = comments_before(lambda.syntax());
if !comments.is_empty() {
println!("--> Doc: {comments}");
}
}
value =
single_match!(lambda.body().unwrap(), ast::Expr::Lambda(lambda) => lambda);
}
println!();
}
}
}
Ok(())
}
fn comments_before(node: &SyntaxNode) -> String {
node.siblings_with_tokens(rowan::Direction::Prev)
// rowan always returns the first node for some reason
.skip(1)
.map_while(|element| match element {
NodeOrToken::Token(token) => match_ast! {
match token {
ast::Comment(it) => Some(Some(it)),
ast::Whitespace(_) => Some(None),
_ => None,
}
},
_ => None,
})
.flatten()
.map(|s| s.text().trim().to_string())
.collect::<Vec<_>>()
.join("\n ")
}
+19
View File
@@ -0,0 +1,19 @@
use std::{env, fs};
fn main() {
let mut iter = env::args().skip(1).peekable();
if iter.peek().is_none() {
eprintln!("Usage: preserve <file>");
return;
}
for file in iter {
let content = match fs::read_to_string(file) {
Ok(content) => content,
Err(err) => {
eprintln!("error reading file: {}", err);
return;
}
};
print!("{}", rnix::Root::parse(&content).tree());
}
}
+48
View File
@@ -0,0 +1,48 @@
use std::{env, error::Error, fs, path::Path};
fn main() -> Result<(), Box<dyn Error>> {
let path = env::var("NIX_PATH")?;
let nixpkgs =
path.split(':').find(|s| s.starts_with("nixpkgs=")).ok_or("no store path found")?;
println!("Nix store path: {}", nixpkgs);
recurse(Path::new(&nixpkgs["nixpkgs=".len()..]))
}
fn recurse(path: &Path) -> Result<(), Box<dyn Error>> {
if path.metadata()?.is_file() {
if path.extension().and_then(|s| s.to_str()) != Some("nix") {
return Ok(());
}
println!("Checking {}", path.display());
let original = fs::read_to_string(path)?;
if original.trim().is_empty() {
return Ok(());
}
let parsed = rnix::Root::parse(&original).tree().to_string();
if original != parsed {
eprintln!("Original input does not match parsed output!");
println!("Input:");
println!("----------");
println!("{}", original);
println!("----------");
println!("Output:");
println!("----------");
println!("{}", parsed);
println!("----------");
return Err("parsing error".into());
}
return Ok(());
} else {
for entry in path.read_dir()? {
let entry = entry?;
if entry.file_type()?.is_symlink() {
continue;
}
recurse(&entry.path())?;
}
}
Ok(())
}
+116
View File
@@ -0,0 +1,116 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1769670296,
"narHash": "sha256-0OzQzsufPwHUTJMAgVf+TXPFG+8x5xrYwz1/M/kI+7o=",
"owner": "nix-community",
"repo": "fenix",
"rev": "b00052920f71c43987b44c804196c083eb0fa1bf",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"import-cargo": {
"locked": {
"lastModified": 1594305518,
"narHash": "sha256-frtArgN42rSaEcEOYWg8sVPMUK+Zgch3c+wejcpX3DY=",
"owner": "edolstra",
"repo": "import-cargo",
"rev": "25d40be4a73d40a2572e0cc233b83253554f06c5",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "import-cargo",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1769461804,
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"import-cargo": "import-cargo",
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1769526882,
"narHash": "sha256-64CZX2vKsfSLKnTCZ0pbc1jPvixWWwuT382g3QHBz5c=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "40cda2f4bd585c1cb99ae4eb025968b8dc2eec42",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+85
View File
@@ -0,0 +1,85 @@
{
description = "Rust-based parser for nix files";
inputs = {
utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
import-cargo.url = "github:edolstra/import-cargo";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, utils, import-cargo, fenix }:
{
overlay = final: prev: let
target = final.stdenv.hostPlatform.rust.rustcTarget;
flags = "--release --offline --target ${target}";
inherit (import-cargo.builders) importCargo;
in {
rnix-parser = final.stdenv.mkDerivation {
pname = "rnix-parser";
version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version;
src = final.lib.cleanSource ./.;
nativeBuildInputs = with final; [
rustc cargo
(importCargo { lockFile = ./Cargo.lock; inherit (final) pkgs; }).cargoHome
];
outputs = [ "out" "doc" ];
doCheck = true;
buildPhase = ''
cargo build ${flags}
cargo doc ${flags}
'';
checkPhase = ''
cargo test ${flags}
cargo bench
'';
installPhase = ''
mkdir -p $out/lib
mkdir -p $doc/share/doc/rnix
cp -r ./target/${target}/doc $doc/share/doc/rnix
cp ./target/${target}/release/librnix.rlib $out/lib/
'';
};
};
}
// utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; };
fenixPkgs = fenix.packages.${system};
nightlyToolchain = fenixPkgs.combine [
fenixPkgs.minimal.cargo
fenixPkgs.minimal.rustc
];
in
rec {
# `nix develop`
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ rustfmt rustc cargo clippy ];
};
# `nix develop .#fuzz` - for running cargo-fuzz
devShells.fuzz = pkgs.mkShell {
buildInputs = [
nightlyToolchain
pkgs.cargo-fuzz
];
# libFuzzer needs libstdc++
LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib";
};
# Backwards compat
devShell = devShells.default;
packages.rnix-parser = pkgs.rnix-parser;
defaultPackage = packages.rnix-parser;
}
);
}
+1
View File
@@ -0,0 +1 @@
use_small_heuristics = "Max"
+9
View File
@@ -0,0 +1,9 @@
let
flake-compat = builtins.fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2";
};
in
(import flake-compat {
src = ./.;
}).shellNix.default
+73
View File
@@ -0,0 +1,73 @@
//! Provides a type system for the AST, in some sense
mod expr_ext;
mod interpol;
mod nodes;
mod operators;
mod path_util;
pub use path_util::Path;
mod str_util;
mod tokens;
use crate::{NixLanguage, SyntaxKind, SyntaxToken};
pub use expr_ext::LiteralKind;
pub use interpol::*;
pub use nodes::*;
pub use operators::{BinOpKind, UnaryOpKind};
pub use tokens::*;
pub trait AstNode: rowan::ast::AstNode<Language = NixLanguage> {}
impl<T> AstNode for T where T: rowan::ast::AstNode<Language = NixLanguage> {}
pub trait AstToken {
fn can_cast(from: SyntaxKind) -> bool
where
Self: Sized;
/// Cast an untyped token into this strongly-typed token. This will return
/// None if the type was not correct.
fn cast(from: SyntaxToken) -> Option<Self>
where
Self: Sized;
fn syntax(&self) -> &SyntaxToken;
}
mod support {
use rowan::ast::AstChildren;
use super::{AstNode, AstToken};
use crate::{SyntaxElement, SyntaxKind, SyntaxToken};
pub(super) fn nth<N: AstNode, NN: AstNode>(parent: &N, n: usize) -> Option<NN> {
parent.syntax().children().flat_map(NN::cast).nth(n)
}
pub(super) fn children<N: AstNode, NN: AstNode>(parent: &N) -> AstChildren<NN> {
rowan::ast::support::children(parent.syntax())
}
pub(super) fn token<N: AstNode, T: AstToken>(parent: &N) -> Option<T> {
children_tokens(parent).next()
}
/// Token untyped
pub(super) fn token_u<N: AstNode>(parent: &N, kind: SyntaxKind) -> Option<SyntaxToken> {
children_tokens_u(parent).find(|it| it.kind() == kind)
}
pub(super) fn children_tokens<N: AstNode, T: AstToken>(parent: &N) -> impl Iterator<Item = T> {
parent
.syntax()
.children_with_tokens()
.filter_map(SyntaxElement::into_token)
.filter_map(T::cast)
}
pub(super) fn children_tokens_u<N: AstNode>(parent: &N) -> impl Iterator<Item = SyntaxToken> {
parent.syntax().children_with_tokens().filter_map(SyntaxElement::into_token)
}
}
+29
View File
@@ -0,0 +1,29 @@
use crate::ast::{self, support::*};
// this is a separate type because it mixes tokens and nodes
// for example, a Str is a node because it can contain nested subexpressions but an Integer is a token.
// This means that we have to write it out manually instead of using the macro to create the type for us.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum LiteralKind {
Float(ast::Float),
Integer(ast::Integer),
Uri(ast::Uri),
}
impl ast::Literal {
pub fn kind(&self) -> LiteralKind {
if let Some(it) = token(self) {
return LiteralKind::Float(it);
}
if let Some(it) = token(self) {
return LiteralKind::Integer(it);
}
if let Some(it) = token(self) {
return LiteralKind::Uri(it);
}
unreachable!()
}
}
+5
View File
@@ -0,0 +1,5 @@
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum InterpolPart<T> {
Literal(T),
Interpolation(super::Interpol),
}
+464
View File
@@ -0,0 +1,464 @@
use crate::{NixLanguage, SyntaxKind, SyntaxKind::*, SyntaxNode, SyntaxToken};
use super::{operators::BinOpKind, support::*, AstNode, UnaryOpKind};
use rowan::ast::{AstChildren, AstNode as OtherAstNode};
pub trait HasEntry: AstNode {
fn entries(&self) -> AstChildren<Entry>
where
Self: Sized,
{
children(self)
}
fn attrpath_values(&self) -> AstChildren<AttrpathValue>
where
Self: Sized,
{
children(self)
}
fn inherits(&self) -> AstChildren<Inherit>
where
Self: Sized,
{
children(self)
}
}
macro_rules! node {
(
#[from($kind:ident)]
$(#[$meta:meta])*
struct $name:ident;
) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
$(#[$meta])*
pub struct $name(pub(super) SyntaxNode);
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self.syntax(), f)
}
}
impl rowan::ast::AstNode for $name {
type Language = crate::NixLanguage;
fn can_cast(kind: crate::SyntaxKind) -> bool {
kind == $kind
}
fn cast(from: SyntaxNode) -> Option<Self> {
if Self::can_cast(from.kind()) {
Some(Self(from))
} else {
None
}
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
impl $name {
pub const KIND: SyntaxKind = $kind;
}
};
(
#[from($($variant:ident),* $(,)?)]
$(#[$meta:meta])*
enum $name:ident;
) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
$(#[$meta])*
pub enum $name {
$(
$variant($variant),
)*
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self.syntax(), f)
}
}
impl rowan::ast::AstNode for $name {
type Language = NixLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
matches!(kind, $($variant::KIND)|*)
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
let res = match syntax.kind() {
$(
$variant::KIND => $name::$variant($variant(syntax))
),*,
_ => return None,
};
Some(res)
}
fn syntax(&self) -> &SyntaxNode {
match self {
$(
$name::$variant(it) => &it.0,
)*
}
}
}
$(
impl From<$variant> for $name {
fn from(node: $variant) -> $name { $name::$variant(node) }
}
impl TryFrom<$name> for $variant {
type Error = ();
fn try_from(node: $name) -> Result<$variant, ()> {
match node {
$name::$variant(it) => Ok(it),
_ => Err(()),
}
}
}
)*
};
}
macro_rules! tg {
(
$(#[doc=$doc:tt])?
$name:ident,
$token:tt
) => {
$(#[doc=$doc])?
pub fn $name(&self) -> Option<SyntaxToken> {
token_u(self, T![$token])
}
};
}
macro_rules! ng {
(
$(#[$meta:meta])*
$name:ident,
$ty:ty,
$i:expr
) => {
$(#[$meta])*
pub fn $name(&self) -> Option<$ty> {
nth(self, $i)
}
};
(
$(#[$meta:meta])*
$name:ident,
[$ty:ty]
) => {
$(#[$meta])*
pub fn $name(&self) -> AstChildren<$ty> {
children(self)
}
};
}
node! { #[from(NODE_LITERAL)] struct Literal; }
node! {
#[from(
Apply,
Assert,
Error,
IfElse,
Select,
Str,
PathAbs,
PathRel,
PathHome,
PathSearch,
Literal,
Lambda,
LegacyLet,
LetIn,
List,
BinOp,
Paren,
Root,
AttrSet,
UnaryOp,
Ident,
With,
HasAttr,
CurPos,
)]
/// An expression. The fundamental nix ast type.
enum Expr;
}
node! {
#[from(
Ident,
Dynamic,
Str,
)]
enum Attr;
}
node! { #[from(NODE_IDENT)] struct Ident; }
impl Ident {
// A NODE_IDENT may either have a nested TOKEN_OR if the identifier was "or" or TOKEN_IDENT for everything else
tg! { ident_token, TOKEN_IDENT }
}
node! { #[from(NODE_APPLY)] struct Apply; }
impl Apply {
ng! { lambda, Expr, 0 }
ng! { argument, Expr, 1 }
}
node! { #[from(NODE_ASSERT)] struct Assert; }
impl Assert {
tg! { assert_token, assert }
ng! { condition, Expr, 0 }
ng! { body, Expr, 1 }
}
node! { #[from(NODE_ATTRPATH)] struct Attrpath; }
impl Attrpath {
ng! { attrs, [Attr] }
}
node! { #[from(NODE_DYNAMIC)] struct Dynamic; }
impl Dynamic {
tg! { interpol_start_token, TOKEN_INTERPOL_START }
ng! { expr, Expr, 0 }
tg! { interpol_end_token, TOKEN_INTERPOL_END }
}
node! { #[from(NODE_ERROR)] struct Error; }
node! { #[from(NODE_IF_ELSE)] struct IfElse; }
impl IfElse {
tg! { if_token, if }
ng! { condition, Expr, 0 }
tg! { then_token, then }
ng! { body, Expr, 1 }
tg! { else_token, else }
ng! { else_body, Expr, 2 }
}
node! { #[from(NODE_SELECT)] struct Select; }
impl Select {
ng! { expr, Expr, 0 }
tg! { dot_token, . }
ng! { attrpath, Attrpath, 0 }
tg! { or_token, or }
ng! { default_expr, Expr, 1 }
}
node! { #[from(NODE_INHERIT)] struct Inherit; }
impl Inherit {
tg! { inherit_token, inherit }
ng! { from, InheritFrom, 0 }
ng! { attrs, [Attr] }
}
node! { #[from(NODE_INHERIT_FROM)] struct InheritFrom; }
impl InheritFrom {
tg! { l_paren_token, '(' }
ng! { expr, Expr, 0 }
tg! { r_paren_token, ')' }
}
node! { #[from(NODE_PATH_ABS)] struct PathAbs; }
node! { #[from(NODE_PATH_REL)] struct PathRel; }
node! { #[from(NODE_PATH_HOME)] struct PathHome; }
node! { #[from(NODE_PATH_SEARCH)] struct PathSearch; }
node! {
#[from(
PathAbs,
PathRel,
PathHome,
PathSearch,
)]
enum Path;
}
node! { #[from(NODE_STRING)] struct Str; }
node! { #[from(NODE_INTERPOL)] struct Interpol; }
impl Interpol {
ng! { expr, Expr, 0 }
}
node! { #[from(NODE_LAMBDA)] struct Lambda; }
impl Lambda {
ng! { param, Param, 0 }
tg! { token_colon, : }
ng! { body, Expr, 0 }
}
node! { #[from(NODE_LEGACY_LET)] struct LegacyLet; }
impl HasEntry for LegacyLet {}
impl LegacyLet {
tg! { let_token, let }
tg! { curly_open_token, '{' }
tg! { curly_close_token, '}' }
}
node! { #[from(NODE_LET_IN)] struct LetIn; }
impl HasEntry for LetIn {}
impl LetIn {
tg! { let_token, let }
tg! { in_token, in }
ng! { body, Expr, 0 }
}
node! { #[from(NODE_LIST)] struct List; }
impl List {
tg! { l_brack_token, '[' }
ng! { items, [Expr] }
tg! { r_brack_token, ']' }
}
node! { #[from(NODE_BIN_OP)] struct BinOp; }
impl BinOp {
ng! { lhs, Expr, 0 }
pub fn operator(&self) -> Option<BinOpKind> {
children_tokens_u(self).find_map(|t| BinOpKind::from_kind(t.kind()))
}
ng! { rhs, Expr, 1 }
}
node! { #[from(NODE_PAREN)] struct Paren; }
impl Paren {
tg! { l_paren_token, '(' }
ng! { expr, Expr, 0 }
tg! { r_paren_token, ')' }
}
node! { #[from(NODE_PAT_BIND)] struct PatBind; }
impl PatBind {
ng! { ident, Ident, 0 }
}
node! { #[from(NODE_PAT_ENTRY)] struct PatEntry; }
impl PatEntry {
ng! { ident, Ident, 0 }
tg! { question_token, ? }
ng! { default, Expr, 1 }
}
node! { #[from(NODE_IDENT_PARAM)] struct IdentParam; }
impl IdentParam {
ng! { ident, Ident, 0 }
}
node! {
#[from(
Pattern,
IdentParam,
)]
enum Param;
}
node! { #[from(NODE_PATTERN)] struct Pattern; }
impl Pattern {
tg! { at_token, @ }
ng! { pat_entries, [PatEntry] }
tg! { ellipsis_token, ... }
ng! { pat_bind, PatBind, 0 }
}
node! { #[from(NODE_ROOT)] struct Root; }
impl Root {
ng! { expr, Expr, 0 }
}
node! { #[from(NODE_ATTR_SET)] struct AttrSet; }
impl HasEntry for AttrSet {}
impl AttrSet {
tg! { rec_token, rec }
tg! { l_curly_token, '{' }
tg! { r_curly_token, '}' }
}
node! {
#[from(
Inherit,
AttrpathValue,
)]
enum Entry;
}
node! { #[from(NODE_ATTRPATH_VALUE)] struct AttrpathValue; }
impl AttrpathValue {
ng! { attrpath, Attrpath, 0 }
tg! { assign_token, = }
ng! { value, Expr, 0 }
}
node! { #[from(NODE_UNARY_OP)] struct UnaryOp; }
impl UnaryOp {
pub fn operator(&self) -> Option<UnaryOpKind> {
children_tokens_u(self).find_map(|t| UnaryOpKind::from_kind(t.kind()))
}
ng! { expr, Expr, 0 }
}
node! { #[from(NODE_WITH)] struct With; }
impl With {
tg! { with_token, with }
ng! { namespace, Expr, 0 }
tg! { semicolon_token, ; }
ng! { body, Expr, 1 }
}
node! { #[from(NODE_HAS_ATTR)] struct HasAttr; }
impl HasAttr {
ng! { expr, Expr, 0 }
tg! { question_token, ? }
ng! { attrpath, Attrpath, 0 }
}
node! { #[from(NODE_CUR_POS)] struct CurPos; }
impl CurPos {
tg! { cur_pos_token, __curPos }
}
+71
View File
@@ -0,0 +1,71 @@
use crate::SyntaxKind::{self, *};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BinOpKind {
Concat,
Update,
Add,
Sub,
Mul,
Div,
And,
Equal,
Implication,
Less,
LessOrEq,
More,
MoreOrEq,
NotEqual,
Or,
PipeRight,
PipeLeft,
}
impl BinOpKind {
/// Get the operation kind from a SyntaxKind in the AST
pub fn from_kind(token: SyntaxKind) -> Option<Self> {
match token {
TOKEN_CONCAT => Some(BinOpKind::Concat),
TOKEN_UPDATE => Some(BinOpKind::Update),
TOKEN_ADD => Some(BinOpKind::Add),
TOKEN_SUB => Some(BinOpKind::Sub),
TOKEN_MUL => Some(BinOpKind::Mul),
TOKEN_DIV => Some(BinOpKind::Div),
TOKEN_AND_AND => Some(BinOpKind::And),
TOKEN_EQUAL => Some(BinOpKind::Equal),
TOKEN_IMPLICATION => Some(BinOpKind::Implication),
TOKEN_LESS => Some(BinOpKind::Less),
TOKEN_LESS_OR_EQ => Some(BinOpKind::LessOrEq),
TOKEN_MORE => Some(BinOpKind::More),
TOKEN_MORE_OR_EQ => Some(BinOpKind::MoreOrEq),
TOKEN_NOT_EQUAL => Some(BinOpKind::NotEqual),
TOKEN_OR_OR => Some(BinOpKind::Or),
TOKEN_PIPE_RIGHT => Some(BinOpKind::PipeRight),
TOKEN_PIPE_LEFT => Some(BinOpKind::PipeLeft),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UnaryOpKind {
Invert,
Negate,
}
impl UnaryOpKind {
/// Get the operation kind from a token in the AST
pub fn from_kind(kind: SyntaxKind) -> Option<Self> {
match kind {
TOKEN_INVERT => Some(UnaryOpKind::Invert),
TOKEN_SUB => Some(UnaryOpKind::Negate),
_ => None,
}
}
}
+204
View File
@@ -0,0 +1,204 @@
use crate::kinds::SyntaxKind::{TOKEN_PATH_ABS, TOKEN_PATH_HOME, TOKEN_PATH_REL};
use rowan::{ast::AstNode as OtherAstNode, NodeOrToken};
pub use super::nodes::Path;
use super::{
nodes::{PathAbs, PathHome, PathRel, PathSearch},
AstToken, InterpolPart, PathContent,
};
use crate::ast;
fn extract_path_parts<T: ast::AstNode>(node: &T) -> Vec<InterpolPart<PathContent>> {
node.syntax()
.children_with_tokens()
.map(|child| match child {
NodeOrToken::Token(token) => {
debug_assert!(matches!(
token.kind(),
TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME
));
InterpolPart::Literal(PathContent::cast(token).unwrap())
}
NodeOrToken::Node(node) => {
InterpolPart::Interpolation(ast::Interpol::cast(node).unwrap())
}
})
.collect()
}
// Direct methods for interpolatable path types
impl PathAbs {
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
extract_path_parts(self)
}
}
impl PathRel {
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
extract_path_parts(self)
}
}
impl PathHome {
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
extract_path_parts(self)
}
}
// Direct methods for search path
impl PathSearch {
/// Get the content of a search path
pub fn content(&self) -> Option<PathContent> {
self.syntax()
.children_with_tokens()
.filter_map(|child| child.into_token().and_then(PathContent::cast))
.next()
}
}
/// Extension methods for the Path enum
impl Path {
/// Get parts from any path type in a unified way
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
match self {
// For interpolatable paths, get their parts
Path::PathAbs(p) => p.parts(),
Path::PathRel(p) => p.parts(),
Path::PathHome(p) => p.parts(),
// For search paths, return a single literal component if content exists
Path::PathSearch(p) => {
if let Some(content) = p.content() {
vec![InterpolPart::Literal(content)]
} else {
vec![]
}
}
}
}
pub fn is_search(&self) -> bool {
matches!(self, Path::PathSearch(_))
}
pub fn is_interpolatable(&self) -> bool {
!self.is_search()
}
}
#[cfg(test)]
mod tests {
use rowan::ast::AstNode;
use super::InterpolPart;
use crate::{
ast::{self, Path},
Root,
};
#[test]
fn test_path_types() {
// Absolute path
let inp = "/foo/bar";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathAbs(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();
assert!(path.is_interpolatable());
assert!(!path.is_search());
}
// Search path
let inp = "<nixpkgs>";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathSearch(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();
assert!(!path.is_interpolatable());
assert!(path.is_search());
}
}
#[test]
fn test_parts() {
// Test parts with absolute path
let inp = "/foo/bar";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathAbs(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();
let parts = path.parts();
assert_eq!(parts.len(), 1);
match &parts[0] {
InterpolPart::Literal(content) => {
assert_eq!(content.text(), "/foo/bar");
}
_ => panic!("Expected literal part"),
}
}
// Test parts with interpolated path
let inp = r#"./a/${"hello"}"#;
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathRel(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();
let parts = path.parts();
assert_eq!(parts.len(), 2);
match &parts[0] {
InterpolPart::Literal(content) => {
assert_eq!(content.text(), "./a/");
}
_ => panic!("Expected literal part"),
}
match &parts[1] {
InterpolPart::Interpolation(_) => {} // Success
_ => panic!("Expected interpolation part"),
}
}
// Test parts with search path
let inp = "<nixpkgs>";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathSearch(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();
let parts = path.parts();
assert_eq!(parts.len(), 1);
match &parts[0] {
InterpolPart::Literal(content) => {
assert_eq!(content.text(), "<nixpkgs>");
}
_ => panic!("Expected literal part"),
}
}
}
#[test]
fn direct_method_usage() {
// Test direct parts() method on PathAbs
let inp = "/foo/bar";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathAbs(p) = expr {
let parts = p.parts();
assert_eq!(parts.len(), 1);
match &parts[0] {
InterpolPart::Literal(content) => {
assert_eq!(content.text(), "/foo/bar");
}
_ => panic!("Expected literal part"),
}
}
// Test direct content() method on PathSearch
let inp = "<nixpkgs>";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathSearch(p) = expr {
let content = p.content().expect("Expected content");
assert_eq!(content.text(), "<nixpkgs>");
}
}
}
+363
View File
@@ -0,0 +1,363 @@
use crate::kinds::SyntaxKind::*;
use rowan::{ast::AstNode as OtherAstNode, NodeOrToken};
use crate::ast;
use super::{support::children_tokens_u, AstToken, InterpolPart, StrContent};
impl ast::Str {
pub fn parts(&self) -> impl Iterator<Item = InterpolPart<StrContent>> {
self.syntax().children_with_tokens().filter_map(|child| match child {
NodeOrToken::Token(token) if token.kind() == TOKEN_STRING_CONTENT => {
Some(InterpolPart::Literal(StrContent::cast(token).unwrap()))
}
NodeOrToken::Token(token) => {
assert!(token.kind() == TOKEN_STRING_START || token.kind() == TOKEN_STRING_END);
None
}
NodeOrToken::Node(node) => {
assert_eq!(node.kind(), NODE_INTERPOL);
Some(InterpolPart::Interpolation(ast::Interpol::cast(node).unwrap()))
}
})
}
pub fn normalized_parts(&self) -> Vec<InterpolPart<String>> {
let multiline = children_tokens_u(self).next().is_some_and(|t| t.text() == "''");
let mut is_first_literal = true;
let mut at_start_of_line = true;
let mut min_indent = 1000000;
let mut cur_indent = 0;
let mut n = 0;
let mut first_is_literal = false;
let parts: Vec<InterpolPart<StrContent>> = self.parts().collect();
if multiline {
for part in &parts {
match part {
InterpolPart::Interpolation(_) => {
if at_start_of_line {
at_start_of_line = false;
min_indent = min_indent.min(cur_indent);
}
n += 1;
}
InterpolPart::Literal(literal) => {
let mut token_text = literal.syntax().text();
if n == 0 {
first_is_literal = true;
}
if is_first_literal && first_is_literal {
is_first_literal = false;
if let Some(p) = token_text.find('\n') {
if token_text[0..p].chars().all(|c| c == ' ') {
token_text = &token_text[p + 1..]
}
}
}
for c in token_text.chars() {
if at_start_of_line {
if c == ' ' {
cur_indent += 1;
} else if c == '\n' {
cur_indent = 0;
} else {
at_start_of_line = false;
min_indent = min_indent.min(cur_indent);
}
} else if c == '\n' {
at_start_of_line = true;
cur_indent = 0;
}
}
n += 1;
}
}
}
}
let mut normalized_parts = Vec::new();
let mut cur_dropped = 0;
let mut i = 0;
is_first_literal = true;
at_start_of_line = true;
for part in parts {
match part {
InterpolPart::Interpolation(interpol) => {
at_start_of_line = false;
cur_dropped = 0;
normalized_parts.push(InterpolPart::Interpolation(interpol));
i += 1;
}
InterpolPart::Literal(literal) => {
let mut token_text = literal.syntax().text();
if multiline {
if is_first_literal && first_is_literal {
is_first_literal = false;
if let Some(p) = token_text.find('\n') {
if token_text[0..p].chars().all(|c| c == ' ') {
token_text = &token_text[p + 1..];
if token_text.is_empty() {
i += 1;
continue;
}
}
}
}
let mut str = String::new();
for c in token_text.chars() {
if at_start_of_line {
if c == ' ' {
if cur_dropped >= min_indent {
str.push(c);
}
cur_dropped += 1;
} else if c == '\n' {
cur_dropped = 0;
str.push(c);
} else {
at_start_of_line = false;
cur_dropped = 0;
str.push(c);
}
} else {
str.push(c);
if c == '\n' {
at_start_of_line = true;
}
}
}
if i == n - 1 {
if let Some(p) = str.rfind('\n') {
if str[p + 1..].chars().all(|c| c == ' ') {
str.truncate(p + 1);
}
}
}
normalized_parts.push(InterpolPart::Literal(unescape(&str, multiline)));
i += 1;
} else {
normalized_parts
.push(InterpolPart::Literal(unescape(token_text, multiline)));
}
}
}
}
normalized_parts
}
}
/// Interpret escape sequences in the nix string and return the converted value
pub fn unescape(input: &str, multiline: bool) -> String {
let mut output = String::new();
let mut input = input.chars().peekable();
loop {
match input.next() {
None => break,
Some('"') if !multiline => break,
Some('\\') if !multiline => match input.next() {
None => break,
Some('n') => output.push('\n'),
Some('r') => output.push('\r'),
Some('t') => output.push('\t'),
Some(c) => output.push(c),
},
Some('\'') if multiline => match input.next() {
None => {
output.push('\'');
}
Some('\'') => match input.peek() {
Some('\'') => {
input.next().unwrap();
output.push_str("''");
}
Some('$') => {
input.next().unwrap();
output.push('$');
}
Some('\\') => {
input.next().unwrap();
match input.next() {
None => break,
Some('n') => output.push('\n'),
Some('r') => output.push('\r'),
Some('t') => output.push('\t'),
Some(c) => output.push(c),
}
}
_ => break,
},
Some(c) => {
output.push('\'');
output.push(c);
}
},
Some(c) => output.push(c),
}
}
output
}
#[cfg(test)]
mod tests {
use crate::Root;
use super::*;
#[test]
fn string_unescapes() {
assert_eq!(unescape(r#"Hello\n\"World\" :D"#, false), "Hello\n\"World\" :D");
assert_eq!(unescape(r#"\"Hello\""#, false), "\"Hello\"");
assert_eq!(unescape(r#"Hello''\n'''World''' :D"#, true), "Hello\n''World'' :D");
assert_eq!(unescape(r#""Hello""#, true), "\"Hello\"");
}
#[test]
fn parts_leading_ws() {
let inp = "''\n hello\n world''";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
match expr {
ast::Expr::Str(str) => {
assert_eq!(
str.normalized_parts(),
vec![InterpolPart::Literal("hello\nworld".to_string())]
)
}
_ => unreachable!(),
}
}
#[test]
fn parts_trailing_ws_single_line() {
let inp = "''hello ''";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
match expr {
ast::Expr::Str(str) => {
assert_eq!(
str.normalized_parts(),
vec![InterpolPart::Literal("hello ".to_string())]
)
}
_ => unreachable!(),
}
}
#[test]
fn parts_trailing_ws_multiline() {
let inp = "''hello\n ''";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
match expr {
ast::Expr::Str(str) => {
assert_eq!(
str.normalized_parts(),
vec![InterpolPart::Literal("hello\n".to_string())]
)
}
_ => unreachable!(),
}
}
#[test]
fn parts() {
use crate::{NixLanguage, SyntaxNode};
use rowan::{GreenNodeBuilder, Language};
fn string_node(content: &str) -> ast::Str {
let mut builder = GreenNodeBuilder::new();
builder.start_node(NixLanguage::kind_to_raw(NODE_STRING));
builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_START), "''");
builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_CONTENT), content);
builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_END), "''");
builder.finish_node();
ast::Str::cast(SyntaxNode::new_root(builder.finish())).unwrap()
}
let txtin = r#"
|trailing-whitespace
|trailing-whitespace
This is a multiline string :D
indented by two
\'\'\'\'\
''${ interpolation was escaped }
two single quotes: '''
three single quotes: ''''
"#
.replace("|trailing-whitespace", "");
if let [InterpolPart::Literal(lit)] =
&ast::Str::normalized_parts(&string_node(txtin.as_str()))[..]
{
assert_eq!(lit,
// Get the below with nix repl
" \n \nThis is a multiline string :D\n indented by two\n\\'\\'\\'\\'\\\n${ interpolation was escaped }\ntwo single quotes: ''\nthree single quotes: '''\n"
);
} else {
unreachable!();
}
}
#[test]
fn parts_ast() {
fn assert_eq_ast_ctn(it: &mut dyn Iterator<Item = InterpolPart<String>>, x: &str) {
let tmp = it.next().expect("unexpected EOF");
if let InterpolPart::Interpolation(astn) = tmp {
assert_eq!(astn.expr().unwrap().syntax().to_string(), x);
} else {
unreachable!("unexpected literal {:?}", tmp);
}
}
let inp = r#"''
This version of Nixpkgs requires Nix >= ${requiredVersion}, please upgrade:
- If you are running NixOS, `nixos-rebuild' can be used to upgrade your system.
- Alternatively, with Nix > 2.0 `nix upgrade-nix' can be used to imperatively
upgrade Nix. You may use `nix-env --version' to check which version you have.
- If you installed Nix using the install script (https://nixos.org/nix/install),
it is safe to upgrade by running it again:
curl -L https://nixos.org/nix/install | sh
For more information, please see the NixOS release notes at
https://nixos.org/nixos/manual or locally at
${toString ./nixos/doc/manual/release-notes}.
If you need further help, see https://nixos.org/nixos/support.html
''"#;
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
match expr {
ast::Expr::Str(s) => {
let mut it = s.normalized_parts().into_iter();
assert_eq!(
it.next().unwrap(),
InterpolPart::Literal("\nThis version of Nixpkgs requires Nix >= ".to_string())
);
assert_eq_ast_ctn(&mut it, "requiredVersion");
assert_eq!(it.next().unwrap(), InterpolPart::Literal(
", please upgrade:\n\n- If you are running NixOS, `nixos-rebuild' can be used to upgrade your system.\n\n- Alternatively, with Nix > 2.0 `nix upgrade-nix' can be used to imperatively\n upgrade Nix. You may use `nix-env --version' to check which version you have.\n\n- If you installed Nix using the install script (https://nixos.org/nix/install),\n it is safe to upgrade by running it again:\n\n curl -L https://nixos.org/nix/install | sh\n\nFor more information, please see the NixOS release notes at\nhttps://nixos.org/nixos/manual or locally at\n".to_string()
));
assert_eq_ast_ctn(&mut it, "toString ./nixos/doc/manual/release-notes");
assert_eq!(
it.next().unwrap(),
InterpolPart::Literal(
".\n\nIf you need further help, see https://nixos.org/nixos/support.html\n"
.to_string()
)
);
}
_ => unreachable!(),
}
}
}
+174
View File
@@ -0,0 +1,174 @@
use core::num;
use crate::{
ast::AstToken,
SyntaxKind::{self, *},
SyntaxToken,
};
macro_rules! token {
(
#[from($kind:ident)]
$(#[$meta:meta])*
struct $name:ident;
) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
$(#[$meta])*
pub struct $name(pub(super) SyntaxToken);
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self.syntax(), f)
}
}
impl AstToken for $name {
fn can_cast(kind: SyntaxKind) -> bool {
$kind == kind
}
fn cast(from: SyntaxToken) -> Option<Self> {
if from.kind() == $kind {
Some(Self(from))
} else {
None
}
}
fn syntax(&self) -> &SyntaxToken {
&self.0
}
}
};
}
token! { #[from(TOKEN_WHITESPACE)] struct Whitespace; }
token! { #[from(TOKEN_COMMENT)] struct Comment; }
impl Comment {
pub fn text(&self) -> &str {
let text = self.syntax().text();
// Handle both "#..." and "/*...*/" comments.
match text.strip_prefix('#') {
Some(s) => s,
None => text.strip_prefix(r#"/*"#).unwrap().strip_suffix(r#"*/"#).unwrap(),
}
}
}
#[cfg(test)]
mod tests {
use crate::ast;
use crate::match_ast;
use crate::Root;
use super::*;
use rowan::ast::AstNode;
#[test]
fn comment() {
let s = "# comment bruh
/* this is a multiline comment wow
asdfasdf
asdfasdf */
1 + 1
/* last one */
";
let comments: Vec<String> = Root::parse(s)
.ok()
.unwrap()
.syntax()
.children_with_tokens()
.filter_map(|e| match e {
rowan::NodeOrToken::Token(token) => match_ast! { match token {
ast::Comment(it) => Some(it.text().to_string()),
_ => None,
}},
rowan::NodeOrToken::Node(_) => None,
})
.collect();
let expected = vec![
" comment bruh",
" this is a multiline comment wow\nasdfasdf\nasdfasdf ",
" last one ",
];
assert_eq!(comments, expected);
}
}
token! { #[from(TOKEN_FLOAT)] struct Float; }
impl Float {
pub fn value(&self) -> Result<f64, num::ParseFloatError> {
self.syntax().text().parse()
}
}
token! { #[from(TOKEN_INTEGER)] struct Integer; }
impl Integer {
pub fn value(&self) -> Result<i64, num::ParseIntError> {
self.syntax().text().parse()
}
}
// A literal part of a path (absolute / relative / home / search) without
// distinguishing which concrete flavour it was taken from.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PathContent(pub(super) SyntaxToken);
impl PathContent {
/// Returns the path as a string
pub fn text(&self) -> &str {
self.syntax().text()
}
/// Returns the kind of path token
pub fn path_kind(&self) -> SyntaxKind {
self.syntax().kind()
}
/// Returns true if this is an absolute path
pub fn is_absolute(&self) -> bool {
self.path_kind() == TOKEN_PATH_ABS
}
/// Returns true if this is a relative path
pub fn is_relative(&self) -> bool {
self.path_kind() == TOKEN_PATH_REL
}
/// Returns true if this is a home-relative path
pub fn is_home(&self) -> bool {
self.path_kind() == TOKEN_PATH_HOME
}
/// Returns true if this is a search path
pub fn is_search(&self) -> bool {
self.path_kind() == TOKEN_PATH_SEARCH
}
}
impl AstToken for PathContent {
fn can_cast(kind: SyntaxKind) -> bool {
matches!(kind, TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME | TOKEN_PATH_SEARCH)
}
fn cast(from: SyntaxToken) -> Option<Self> {
if Self::can_cast(from.kind()) {
Some(Self(from))
} else {
None
}
}
fn syntax(&self) -> &SyntaxToken {
&self.0
}
}
token! { #[from(TOKEN_STRING_CONTENT)] struct StrContent; }
token! { #[from(TOKEN_URI)] struct Uri; }
+285
View File
@@ -0,0 +1,285 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u16)]
#[allow(non_camel_case_types)]
#[non_exhaustive]
pub enum SyntaxKind {
TOKEN_ADD,
TOKEN_AND_AND,
TOKEN_ASSIGN,
TOKEN_ASSERT,
TOKEN_AT,
TOKEN_COLON,
TOKEN_COMMA,
TOKEN_COMMENT,
TOKEN_CONCAT,
TOKEN_CUR_POS,
TOKEN_DIV,
TOKEN_DOT,
TOKEN_ELLIPSIS,
TOKEN_ELSE,
TOKEN_EQUAL,
TOKEN_ERROR,
TOKEN_FLOAT,
TOKEN_IDENT,
TOKEN_IF,
TOKEN_IMPLICATION,
TOKEN_IN,
TOKEN_INHERIT,
TOKEN_INTEGER,
TOKEN_INTERPOL_END,
TOKEN_INTERPOL_START,
TOKEN_INVERT,
TOKEN_L_BRACE,
TOKEN_L_BRACK,
TOKEN_L_PAREN,
TOKEN_LESS,
TOKEN_LESS_OR_EQ,
TOKEN_LET,
TOKEN_MORE,
TOKEN_MORE_OR_EQ,
TOKEN_MUL,
TOKEN_NOT_EQUAL,
TOKEN_OR,
TOKEN_OR_OR,
TOKEN_PATH_ABS,
TOKEN_PATH_HOME,
TOKEN_PATH_REL,
TOKEN_PATH_SEARCH,
TOKEN_PIPE_LEFT,
TOKEN_PIPE_RIGHT,
TOKEN_QUESTION,
TOKEN_R_BRACE,
TOKEN_R_BRACK,
TOKEN_R_PAREN,
TOKEN_REC,
TOKEN_SEMICOLON,
TOKEN_STRING_CONTENT,
TOKEN_STRING_END,
TOKEN_STRING_START,
TOKEN_SUB,
TOKEN_THEN,
TOKEN_UPDATE,
TOKEN_URI,
TOKEN_WHITESPACE,
TOKEN_WITH,
NODE_APPLY,
NODE_ASSERT,
NODE_ATTR_SET,
NODE_ATTRPATH,
NODE_ATTRPATH_VALUE,
NODE_BIN_OP,
NODE_CUR_POS,
NODE_DYNAMIC,
NODE_ERROR,
// Attrpath existence check: foo ? bar.${baz}."bux"
NODE_HAS_ATTR,
NODE_IDENT,
NODE_IDENT_PARAM,
NODE_IF_ELSE,
NODE_INHERIT,
NODE_INHERIT_FROM,
NODE_INTERPOL,
NODE_LAMBDA,
// An old let { x = 92; body = x; } syntax
NODE_LEGACY_LET,
NODE_LET_IN,
NODE_LIST,
NODE_LITERAL,
NODE_PAREN,
NODE_PAT_BIND,
NODE_PAT_ENTRY,
NODE_PATTERN,
NODE_PATH_ABS,
NODE_PATH_HOME,
NODE_PATH_REL,
NODE_PATH_SEARCH,
NODE_ROOT,
NODE_SELECT,
NODE_STRING,
NODE_UNARY_OP,
NODE_WITH,
}
use SyntaxKind::*;
/// Maximum valid discriminant value for SyntaxKind
pub(crate) const SYNTAX_KIND_MAX: u16 = NODE_WITH as u16;
/// Error returned when parsing a string into a `SyntaxKind` fails.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseSyntaxKindError;
impl std::fmt::Display for ParseSyntaxKindError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid SyntaxKind")
}
}
impl std::error::Error for ParseSyntaxKindError {}
impl std::str::FromStr for SyntaxKind {
type Err = ParseSyntaxKindError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"TOKEN_ADD" => Ok(TOKEN_ADD),
"TOKEN_AND_AND" => Ok(TOKEN_AND_AND),
"TOKEN_ASSIGN" => Ok(TOKEN_ASSIGN),
"TOKEN_ASSERT" => Ok(TOKEN_ASSERT),
"TOKEN_AT" => Ok(TOKEN_AT),
"TOKEN_COLON" => Ok(TOKEN_COLON),
"TOKEN_COMMA" => Ok(TOKEN_COMMA),
"TOKEN_COMMENT" => Ok(TOKEN_COMMENT),
"TOKEN_CONCAT" => Ok(TOKEN_CONCAT),
"TOKEN_CUR_POS" => Ok(TOKEN_CUR_POS),
"TOKEN_DIV" => Ok(TOKEN_DIV),
"TOKEN_DOT" => Ok(TOKEN_DOT),
"TOKEN_ELLIPSIS" => Ok(TOKEN_ELLIPSIS),
"TOKEN_ELSE" => Ok(TOKEN_ELSE),
"TOKEN_EQUAL" => Ok(TOKEN_EQUAL),
"TOKEN_ERROR" => Ok(TOKEN_ERROR),
"TOKEN_FLOAT" => Ok(TOKEN_FLOAT),
"TOKEN_IDENT" => Ok(TOKEN_IDENT),
"TOKEN_IF" => Ok(TOKEN_IF),
"TOKEN_IMPLICATION" => Ok(TOKEN_IMPLICATION),
"TOKEN_IN" => Ok(TOKEN_IN),
"TOKEN_INHERIT" => Ok(TOKEN_INHERIT),
"TOKEN_INTEGER" => Ok(TOKEN_INTEGER),
"TOKEN_INTERPOL_END" => Ok(TOKEN_INTERPOL_END),
"TOKEN_INTERPOL_START" => Ok(TOKEN_INTERPOL_START),
"TOKEN_INVERT" => Ok(TOKEN_INVERT),
"TOKEN_L_BRACE" => Ok(TOKEN_L_BRACE),
"TOKEN_L_BRACK" => Ok(TOKEN_L_BRACK),
"TOKEN_L_PAREN" => Ok(TOKEN_L_PAREN),
"TOKEN_LESS" => Ok(TOKEN_LESS),
"TOKEN_LESS_OR_EQ" => Ok(TOKEN_LESS_OR_EQ),
"TOKEN_LET" => Ok(TOKEN_LET),
"TOKEN_MORE" => Ok(TOKEN_MORE),
"TOKEN_MORE_OR_EQ" => Ok(TOKEN_MORE_OR_EQ),
"TOKEN_MUL" => Ok(TOKEN_MUL),
"TOKEN_NOT_EQUAL" => Ok(TOKEN_NOT_EQUAL),
"TOKEN_OR" => Ok(TOKEN_OR),
"TOKEN_OR_OR" => Ok(TOKEN_OR_OR),
"TOKEN_PATH_ABS" => Ok(TOKEN_PATH_ABS),
"TOKEN_PATH_HOME" => Ok(TOKEN_PATH_HOME),
"TOKEN_PATH_REL" => Ok(TOKEN_PATH_REL),
"TOKEN_PATH_SEARCH" => Ok(TOKEN_PATH_SEARCH),
"TOKEN_PIPE_LEFT" => Ok(TOKEN_PIPE_LEFT),
"TOKEN_PIPE_RIGHT" => Ok(TOKEN_PIPE_RIGHT),
"TOKEN_QUESTION" => Ok(TOKEN_QUESTION),
"TOKEN_R_BRACE" => Ok(TOKEN_R_BRACE),
"TOKEN_R_BRACK" => Ok(TOKEN_R_BRACK),
"TOKEN_R_PAREN" => Ok(TOKEN_R_PAREN),
"TOKEN_REC" => Ok(TOKEN_REC),
"TOKEN_SEMICOLON" => Ok(TOKEN_SEMICOLON),
"TOKEN_STRING_CONTENT" => Ok(TOKEN_STRING_CONTENT),
"TOKEN_STRING_END" => Ok(TOKEN_STRING_END),
"TOKEN_STRING_START" => Ok(TOKEN_STRING_START),
"TOKEN_SUB" => Ok(TOKEN_SUB),
"TOKEN_THEN" => Ok(TOKEN_THEN),
"TOKEN_UPDATE" => Ok(TOKEN_UPDATE),
"TOKEN_URI" => Ok(TOKEN_URI),
"TOKEN_WHITESPACE" => Ok(TOKEN_WHITESPACE),
"TOKEN_WITH" => Ok(TOKEN_WITH),
"NODE_APPLY" => Ok(NODE_APPLY),
"NODE_ASSERT" => Ok(NODE_ASSERT),
"NODE_ATTR_SET" => Ok(NODE_ATTR_SET),
"NODE_ATTRPATH" => Ok(NODE_ATTRPATH),
"NODE_ATTRPATH_VALUE" => Ok(NODE_ATTRPATH_VALUE),
"NODE_BIN_OP" => Ok(NODE_BIN_OP),
"NODE_CUR_POS" => Ok(NODE_CUR_POS),
"NODE_DYNAMIC" => Ok(NODE_DYNAMIC),
"NODE_ERROR" => Ok(NODE_ERROR),
"NODE_HAS_ATTR" => Ok(NODE_HAS_ATTR),
"NODE_IDENT" => Ok(NODE_IDENT),
"NODE_IDENT_PARAM" => Ok(NODE_IDENT_PARAM),
"NODE_IF_ELSE" => Ok(NODE_IF_ELSE),
"NODE_INHERIT" => Ok(NODE_INHERIT),
"NODE_INHERIT_FROM" => Ok(NODE_INHERIT_FROM),
"NODE_INTERPOL" => Ok(NODE_INTERPOL),
"NODE_LAMBDA" => Ok(NODE_LAMBDA),
"NODE_LEGACY_LET" => Ok(NODE_LEGACY_LET),
"NODE_LET_IN" => Ok(NODE_LET_IN),
"NODE_LIST" => Ok(NODE_LIST),
"NODE_LITERAL" => Ok(NODE_LITERAL),
"NODE_PAREN" => Ok(NODE_PAREN),
"NODE_PAT_BIND" => Ok(NODE_PAT_BIND),
"NODE_PAT_ENTRY" => Ok(NODE_PAT_ENTRY),
"NODE_PATTERN" => Ok(NODE_PATTERN),
"NODE_PATH_ABS" => Ok(NODE_PATH_ABS),
"NODE_PATH_HOME" => Ok(NODE_PATH_HOME),
"NODE_PATH_REL" => Ok(NODE_PATH_REL),
"NODE_PATH_SEARCH" => Ok(NODE_PATH_SEARCH),
"NODE_ROOT" => Ok(NODE_ROOT),
"NODE_SELECT" => Ok(NODE_SELECT),
"NODE_STRING" => Ok(NODE_STRING),
"NODE_UNARY_OP" => Ok(NODE_UNARY_OP),
"NODE_WITH" => Ok(NODE_WITH),
_ => Err(ParseSyntaxKindError),
}
}
}
impl SyntaxKind {
/// Returns true if this token is a literal, such as an integer or a string
pub fn is_literal(self) -> bool {
matches!(
self,
TOKEN_FLOAT
| TOKEN_INTEGER
| TOKEN_PATH_ABS
| TOKEN_PATH_REL
| TOKEN_PATH_HOME
| TOKEN_PATH_SEARCH
| TOKEN_URI
)
}
/// Returns true if this token should be used as a function argument.
/// ```ignore
/// Example:
/// add 1 2 + 3
/// ^ ^ ^ ^
/// | | | +- false
/// | | +- true
/// | +- true
/// +- true
/// ```
pub fn is_fn_arg(self) -> bool {
match self {
// NB: the tokenizer always emits TOKEN_IDENT for __curPos;
// TOKEN_CUR_POS is included for completeness but cannot
// currently be produced by the tokenizer.
TOKEN_REC | TOKEN_L_BRACE | TOKEN_L_BRACK | TOKEN_L_PAREN | TOKEN_STRING_START
| TOKEN_IDENT | TOKEN_CUR_POS => true,
_ => self.is_literal(),
}
}
/// Returns true if this token is a comment, whitespace, or similar, and
/// should be skipped over by the parser.
pub fn is_trivia(self) -> bool {
matches!(self, TOKEN_COMMENT | TOKEN_WHITESPACE)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn from_str_valid() {
assert_eq!(SyntaxKind::from_str("TOKEN_COMMENT").unwrap(), TOKEN_COMMENT);
assert_eq!(SyntaxKind::from_str("NODE_ROOT").unwrap(), NODE_ROOT);
assert_eq!(SyntaxKind::from_str("NODE_HAS_ATTR").unwrap(), NODE_HAS_ATTR);
}
#[test]
fn from_str_invalid() {
assert!(SyntaxKind::from_str("INVALID").is_err());
assert!(SyntaxKind::from_str("").is_err());
assert!(SyntaxKind::from_str("token_comment").is_err()); // case sensitive
}
}
+111
View File
@@ -0,0 +1,111 @@
#[macro_use]
mod macros;
pub mod ast;
mod kinds;
mod parser;
#[cfg(test)]
mod tests;
mod token_set;
mod tokenizer;
use std::marker::PhantomData;
use self::kinds::SYNTAX_KIND_MAX;
pub use self::{kinds::ParseSyntaxKindError, kinds::SyntaxKind, tokenizer::tokenize};
use ast::AstNode;
pub use parser::ParseError;
use rowan::GreenNode;
pub use rowan::{NodeOrToken, TextRange, TextSize, TokenAtOffset, WalkEvent};
pub(crate) use token_set::TokenSet;
pub use tokenizer::Token;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NixLanguage {}
impl rowan::Language for NixLanguage {
type Kind = SyntaxKind;
fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
let discriminant: u16 = raw.0;
assert!(discriminant <= SYNTAX_KIND_MAX);
unsafe { std::mem::transmute::<u16, SyntaxKind>(discriminant) }
}
fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
rowan::SyntaxKind(kind as u16)
}
}
pub type SyntaxNode = rowan::SyntaxNode<NixLanguage>;
pub type SyntaxToken = rowan::SyntaxToken<NixLanguage>;
pub type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
pub type SyntaxElementChildren = rowan::SyntaxElementChildren<NixLanguage>;
pub type SyntaxNodeChildren = rowan::SyntaxNodeChildren<NixLanguage>;
pub use ast::Root;
impl Root {
pub fn parse(s: &str) -> Parse<Root> {
let (green, errors) = parser::parse(tokenize(s));
Parse { green, errors, _ty: PhantomData }
}
}
/// The result of a parse
#[derive(Clone)]
pub struct Parse<T> {
green: GreenNode,
errors: Vec<ParseError>,
_ty: PhantomData<fn() -> T>,
}
impl<T> Parse<T> {
pub fn syntax(&self) -> SyntaxNode {
SyntaxNode::new_root(self.green.clone())
}
}
impl<T: AstNode> Parse<T> {
pub fn tree(&self) -> T {
T::cast(self.syntax()).unwrap()
}
/// Return all errors in the tree, if any
pub fn errors(&self) -> &[ParseError] {
&self.errors
}
/// Either return the first error in the tree, or if there are none return self
pub fn ok(self) -> Result<T, ParseError> {
if let Some(err) = self.errors().first() {
return Err(err.clone());
}
Ok(self.tree())
}
}
/// Matches a `SyntaxNode` against an `ast` type.
///
/// # Example:
///
/// ```ignore
/// match_ast! {
/// match node {
/// ast::Apply(it) => { ... },
/// ast::Assert(it) => { ... },
/// ast::IfElse(it) => { ... },
/// _ => None,
/// }
/// }
/// ```
#[macro_export]
macro_rules! match_ast {
(match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) };
(match ($node:expr) {
$( ast::$ast:ident($it:pat) => $res:expr, )*
_ => $catch_all:expr $(,)?
}) => {{
$( if let Some($it) = ast::$ast::cast($node.clone()) { $res } else )*
{ $catch_all }
}};
}
+52
View File
@@ -0,0 +1,52 @@
#[macro_export]
#[rustfmt::skip]
macro_rules! T {
(assert) => ($crate::SyntaxKind::TOKEN_ASSERT);
(else) => ($crate::SyntaxKind::TOKEN_ELSE);
(if) => ($crate::SyntaxKind::TOKEN_IF);
(in) => ($crate::SyntaxKind::TOKEN_IN);
(inherit) => ($crate::SyntaxKind::TOKEN_INHERIT);
(let) => ($crate::SyntaxKind::TOKEN_LET);
(or) => ($crate::SyntaxKind::TOKEN_OR);
(rec) => ($crate::SyntaxKind::TOKEN_REC);
(then) => ($crate::SyntaxKind::TOKEN_THEN);
(with) => ($crate::SyntaxKind::TOKEN_WITH);
(__curPos) => ($crate::SyntaxKind::TOKEN_CUR_POS);
('{') => ($crate::SyntaxKind::TOKEN_L_BRACE);
('}') => ($crate::SyntaxKind::TOKEN_R_BRACE);
('[') => ($crate::SyntaxKind::TOKEN_L_BRACK);
(']') => ($crate::SyntaxKind::TOKEN_R_BRACK);
('(') => ($crate::SyntaxKind::TOKEN_L_PAREN);
(')') => ($crate::SyntaxKind::TOKEN_R_PAREN);
(=) => ($crate::SyntaxKind::TOKEN_ASSIGN);
(@) => ($crate::SyntaxKind::TOKEN_AT);
(:) => ($crate::SyntaxKind::TOKEN_COLON);
(,) => ($crate::SyntaxKind::TOKEN_COMMA);
(.) => ($crate::SyntaxKind::TOKEN_DOT);
(...) => ($crate::SyntaxKind::TOKEN_ELLIPSIS);
(?) => ($crate::SyntaxKind::TOKEN_QUESTION);
(;) => ($crate::SyntaxKind::TOKEN_SEMICOLON);
(++) => ($crate::SyntaxKind::TOKEN_CONCAT);
(!) => ($crate::SyntaxKind::TOKEN_INVERT);
("//") => ($crate::SyntaxKind::TOKEN_UPDATE);
(+) => ($crate::SyntaxKind::TOKEN_ADD);
(-) => ($crate::SyntaxKind::TOKEN_SUB);
(*) => ($crate::SyntaxKind::TOKEN_MUL);
(/) => ($crate::SyntaxKind::TOKEN_DIV);
(&&) => ($crate::SyntaxKind::TOKEN_AND_AND);
(==) => ($crate::SyntaxKind::TOKEN_EQUAL);
(->) => ($crate::SyntaxKind::TOKEN_IMPLICATION);
(<) => ($crate::SyntaxKind::TOKEN_LESS);
(<=) => ($crate::SyntaxKind::TOKEN_LESS_OR_EQ);
(>) => ($crate::SyntaxKind::TOKEN_MORE);
(>=) => ($crate::SyntaxKind::TOKEN_MORE_OR_EQ);
(!=) => ($crate::SyntaxKind::TOKEN_NOT_EQUAL);
(||) => ($crate::SyntaxKind::TOKEN_OR_OR);
("|>") => ($crate::SyntaxKind::TOKEN_PIPE_RIGHT);
("<|") => ($crate::SyntaxKind::TOKEN_PIPE_LEFT);
($kind:ident) => ($crate::SyntaxKind::$kind);
}
+834
View File
@@ -0,0 +1,834 @@
//! The parser: turns a series of tokens into an AST
use std::{collections::VecDeque, fmt};
use rowan::{Checkpoint, GreenNode, GreenNodeBuilder, Language, TextRange, TextSize};
use crate::{
tokenizer::Token,
NixLanguage,
SyntaxKind::{self, *},
TokenSet,
};
/// An error that occurred during parsing
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum ParseError {
/// Unexpected is used when the cause cannot be specified further
Unexpected(TextRange),
/// UnexpectedExtra is used when there are additional tokens to the root in the tree
UnexpectedExtra(TextRange),
/// UnexpectedWanted is used when specific tokens are expected, but different one is found
UnexpectedWanted(SyntaxKind, TextRange, Box<[SyntaxKind]>),
/// UnexpectedDoubleBind is used when a pattern is bound twice
UnexpectedDoubleBind(TextRange),
/// UnexpectedEOF is used when the end of file is reached, while tokens are still expected
UnexpectedEOF,
/// UnexpectedEOFWanted is used when specific tokens are expected, but the end of file is reached
UnexpectedEOFWanted(Box<[SyntaxKind]>),
/// DuplicatedArgs is used when formal arguments are duplicated, e.g. `{ a, a }`
DuplicatedArgs(TextRange, String),
/// RecursionLimitExceeded is used when we're unable to parse further due to likely being close to
/// a stack overflow.
RecursionLimitExceeded,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::Unexpected(range) => {
write!(
f,
"error node at {}..{}",
usize::from(range.start()),
usize::from(range.end())
)
}
ParseError::UnexpectedExtra(range) => {
write!(
f,
"unexpected token at {}..{}",
usize::from(range.start()),
usize::from(range.end())
)
}
ParseError::UnexpectedWanted(got, range, kinds) => write!(
f,
"unexpected {:?} at {}..{}, wanted any of {:?}",
got,
usize::from(range.start()),
usize::from(range.end()),
kinds
),
ParseError::UnexpectedDoubleBind(range) => {
write!(
f,
"unexpected double bind at {}..{}",
usize::from(range.start()),
usize::from(range.end())
)
}
ParseError::UnexpectedEOF => write!(f, "unexpected end of file"),
ParseError::UnexpectedEOFWanted(kinds) => {
write!(f, "unexpected end of file, wanted any of {:?}", kinds)
}
ParseError::DuplicatedArgs(range, ident) => {
write!(
f,
"argument `{}` is duplicated in {}..{}",
ident,
usize::from(range.start()),
usize::from(range.end())
)
}
ParseError::RecursionLimitExceeded => write!(f, "recursion limit exceeded"),
}
}
}
impl std::error::Error for ParseError {}
struct Parser<'a, I>
where
I: Iterator<Item = Token<'a>>,
{
builder: GreenNodeBuilder<'static>,
errors: Vec<ParseError>,
trivia_buffer: Vec<Token<'a>>,
buffer: VecDeque<Token<'a>>,
iter: I,
consumed: TextSize,
// Recursion depth, used for avoiding stack overflows. This may be incremented
// by any method as long as it is decremented when that method returns.
depth: u32,
}
impl<'a, I> Parser<'a, I>
where
I: Iterator<Item = Token<'a>>,
{
fn new(iter: I) -> Self {
Self {
builder: GreenNodeBuilder::new(),
errors: Vec::new(),
trivia_buffer: Vec::with_capacity(1),
buffer: VecDeque::with_capacity(1),
iter,
consumed: TextSize::from(0),
depth: 0,
}
}
fn get_text_position(&self) -> TextSize {
self.consumed
}
fn peek_raw(&mut self) -> Option<&Token<'a>> {
if self.buffer.is_empty() {
if let Some(token) = self.iter.next() {
self.buffer.push_back(token);
}
}
self.buffer.front()
}
fn drain_trivia_buffer(&mut self) {
for (t, s) in self.trivia_buffer.drain(..) {
self.consumed += TextSize::of(s);
self.builder.token(NixLanguage::kind_to_raw(t), s);
}
}
fn eat_trivia(&mut self) {
self.peek();
self.drain_trivia_buffer();
}
fn start_node(&mut self, kind: SyntaxKind) {
self.eat_trivia();
self.builder.start_node(NixLanguage::kind_to_raw(kind));
}
fn checkpoint(&mut self) -> Checkpoint {
self.eat_trivia();
self.builder.checkpoint()
}
fn start_node_at(&mut self, checkpoint: Checkpoint, kind: SyntaxKind) {
self.builder.start_node_at(checkpoint, NixLanguage::kind_to_raw(kind));
}
fn finish_node(&mut self) {
self.builder.finish_node();
}
fn start_error_node(&mut self) -> TextSize {
self.start_node(NODE_ERROR);
self.get_text_position()
}
fn finish_error_node(&mut self) -> TextSize {
self.finish_node();
self.get_text_position()
}
fn bump(&mut self) {
match self.try_next() {
Some((token, s)) => {
if token.is_trivia() {
self.trivia_buffer.push((token, s))
} else {
self.drain_trivia_buffer();
self.manual_bump(s, token);
}
}
None => self.errors.push(ParseError::UnexpectedEOF),
}
}
fn try_next(&mut self) -> Option<Token<'a>> {
self.buffer.pop_front().or_else(|| self.iter.next())
}
fn manual_bump(&mut self, s: &str, token: SyntaxKind) {
self.consumed += TextSize::of(s);
self.builder.token(NixLanguage::kind_to_raw(token), s)
}
fn peek(&mut self) -> Option<SyntaxKind> {
while self.peek_raw().map(|&(t, _)| t.is_trivia()).unwrap_or(false) {
self.bump();
}
self.peek_ahead(0)
}
fn peek_ahead(&mut self, n: usize) -> Option<SyntaxKind> {
let mut non_trivia_seen = 0;
let mut cursor = 0;
loop {
if self.buffer.len() <= cursor {
self.buffer.push_back(self.iter.next()?);
}
let kind = self.buffer[cursor].0;
cursor += 1;
if !kind.is_trivia() {
if non_trivia_seen == n {
return Some(kind);
}
non_trivia_seen += 1;
}
}
}
fn expect_peek_any(&mut self, allowed_slice: &[SyntaxKind]) -> Option<SyntaxKind> {
let allowed = TokenSet::from_slice(allowed_slice);
let next = match self.peek() {
None => None,
Some(kind) if allowed.contains(kind) => Some(kind),
Some(kind) => {
let start = self.start_error_node();
loop {
self.bump();
if self.peek().map(|kind| allowed.contains(kind)).unwrap_or(true) {
break;
}
}
let end = self.finish_error_node();
self.errors.push(ParseError::UnexpectedWanted(
kind,
TextRange::new(start, end),
allowed_slice.to_vec().into_boxed_slice(),
));
self.peek()
}
};
if next.is_none() {
self.errors
.push(ParseError::UnexpectedEOFWanted(allowed_slice.to_vec().into_boxed_slice()));
}
next
}
fn expect(&mut self, expected: SyntaxKind) {
if self.expect_peek_any(&[expected]).is_some() {
self.bump();
}
}
fn expect_ident(&mut self) {
if self.expect_peek_any(&[TOKEN_IDENT]).is_some() {
self.start_node(NODE_IDENT);
self.bump();
self.finish_node()
}
}
fn parse_dynamic(&mut self) {
self.start_node(NODE_DYNAMIC);
self.bump();
while self.peek().map(|t| t != TOKEN_INTERPOL_END).unwrap_or(false) {
self.parse_expr();
}
self.bump();
self.finish_node();
}
fn parse_string(&mut self) {
self.start_node(NODE_STRING);
self.expect(TOKEN_STRING_START);
loop {
match self.expect_peek_any(&[
TOKEN_STRING_END,
TOKEN_STRING_CONTENT,
TOKEN_INTERPOL_START,
]) {
Some(TOKEN_STRING_CONTENT) => self.bump(),
Some(TOKEN_INTERPOL_START) => {
self.start_node(NODE_INTERPOL);
self.bump();
self.parse_expr();
self.expect(TOKEN_INTERPOL_END);
self.finish_node();
}
// handled by expect_peek_any
_ => break,
}
}
self.expect(TOKEN_STRING_END);
self.finish_node();
}
fn parse_attr(&mut self) {
match self.peek() {
Some(TOKEN_INTERPOL_START) => self.parse_dynamic(),
Some(TOKEN_STRING_START) => self.parse_string(),
_ => {
// NB: the tokenizer always emits TOKEN_IDENT for __curPos;
// TOKEN_CUR_POS is included for completeness but cannot
// currently be produced by the tokenizer.
if self.expect_peek_any(&[TOKEN_IDENT, TOKEN_OR, TOKEN_CUR_POS]).is_some() {
self.start_node(NODE_IDENT);
let (_, s) = self.try_next().unwrap();
self.manual_bump(s, TOKEN_IDENT);
self.finish_node()
}
}
}
}
fn parse_attrpath(&mut self) {
self.start_node(NODE_ATTRPATH);
loop {
self.parse_attr();
if self.peek() == Some(T![.]) {
self.bump();
} else {
break;
}
}
self.finish_node();
}
fn parse_pattern(&mut self, bound: bool) {
if self.peek().map(|t| t == T!['}']).unwrap_or(true) {
self.bump();
} else {
loop {
match self.expect_peek_any(&[T!['}'], T![...], TOKEN_IDENT]) {
Some(T!['}']) => {
self.bump();
break;
}
Some(T![...]) => {
self.bump();
self.expect(T!['}']);
break;
}
Some(TOKEN_IDENT) => {
self.start_node(NODE_PAT_ENTRY);
self.expect_ident();
if let Some(T![?]) = self.peek() {
self.bump();
self.parse_expr();
}
self.finish_node();
match self.peek() {
Some(T![,]) => self.bump(),
_ => {
self.expect(T!['}']);
break;
}
}
}
// handled by expect_peek_any
_ => break,
}
}
}
if self.peek() == Some(T![@]) {
let kind = if bound { NODE_ERROR } else { NODE_PAT_BIND };
self.start_node(kind);
let start = self.get_text_position();
self.bump();
self.expect_ident();
let end = self.finish_error_node();
if bound {
self.errors.push(ParseError::UnexpectedDoubleBind(TextRange::new(start, end)));
}
}
}
fn parse_set(&mut self, until: SyntaxKind) {
loop {
match self.peek() {
None => break,
token if token == Some(until) => break,
Some(T![inherit]) => {
self.start_node(NODE_INHERIT);
self.bump();
if self.peek() == Some(T!['(']) {
self.start_node(NODE_INHERIT_FROM);
self.bump();
self.parse_expr();
self.expect(T![')']);
self.finish_node();
}
loop {
match self.peek() {
Some(t) if t != T![;] => {
self.parse_attr();
}
Some(_) => {
break;
}
None => {
self.errors.push(ParseError::UnexpectedEOF);
break;
}
}
}
self.expect(T![;]);
self.finish_node();
}
Some(_) => {
self.start_node(NODE_ATTRPATH_VALUE);
self.parse_attrpath();
self.expect(T![=]);
self.parse_expr();
self.expect(T![;]);
self.finish_node();
}
}
}
self.bump(); // the final close, like '}'
}
fn parse_simple(&mut self) -> Checkpoint {
let peek = match self.peek() {
Some(it) => it,
None => {
self.errors.push(ParseError::UnexpectedEOF);
// NB: we don't use `self.checkpoint()` here in order to avoid
// eating the whitespace. The actual checkpoint doesn't matter
// in this case and, ideally, should be returning `None`, but
// that makes code slightly more complex for little real
// benefit.
return self.builder.checkpoint();
}
};
let checkpoint = self.checkpoint();
match peek {
T!['('] => {
self.start_node(NODE_PAREN);
self.bump();
self.parse_expr();
self.bump();
self.finish_node();
}
T![rec] => {
self.start_node(NODE_ATTR_SET);
self.bump();
self.expect(T!['{']);
self.parse_set(T!['}']);
self.finish_node();
}
T!['{'] => {
// Do a lookahead:
let mut peek = [None, None];
for i in &mut peek {
let mut token;
*i = loop {
token = self.iter.next();
let kind = token.as_ref().map(|&(t, _)| t);
if let Some(token) = token {
self.buffer.push_back(token);
}
if kind.map(|t| !t.is_trivia()).unwrap_or(true) {
break kind;
}
};
}
match peek {
[Some(TOKEN_IDENT), Some(T![,])]
| [Some(TOKEN_IDENT), Some(T![?])]
| [Some(TOKEN_IDENT), Some(T!['}'])]
| [Some(T![...]), Some(T!['}'])]
| [Some(T!['}']), Some(T![:])]
| [Some(T!['}']), Some(T![@])] => {
// This looks like a pattern
self.start_node(NODE_LAMBDA);
self.start_node(NODE_PATTERN);
self.bump();
self.parse_pattern(false);
self.finish_node();
self.expect(T![:]);
self.parse_expr();
self.finish_node();
}
_ => {
// This looks like a set
self.start_node(NODE_ATTR_SET);
self.bump();
self.parse_set(T!['}']);
self.finish_node();
}
}
}
T!['['] => {
self.start_node(NODE_LIST);
self.bump();
while self.peek().map(|t| t != T![']']).unwrap_or(false) {
self.parse_simple();
}
self.bump();
self.finish_node();
}
TOKEN_STRING_START => self.parse_string(),
TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME | TOKEN_PATH_SEARCH => {
let node_kind = match self.peek().unwrap() {
TOKEN_PATH_ABS => NODE_PATH_ABS,
TOKEN_PATH_REL => NODE_PATH_REL,
TOKEN_PATH_HOME => NODE_PATH_HOME,
TOKEN_PATH_SEARCH => NODE_PATH_SEARCH,
_ => unreachable!(),
};
self.start_node(node_kind);
self.bump();
// Search paths (<nixpkgs>) don't support interpolation
let is_search_path = node_kind == NODE_PATH_SEARCH;
let is_complex_path = !is_search_path && self.peek() == Some(TOKEN_INTERPOL_START);
if is_complex_path {
loop {
match self.peek_raw().map(|(t, _)| t) {
Some(TOKEN_PATH_ABS) | Some(TOKEN_PATH_REL) | Some(TOKEN_PATH_HOME) => {
self.bump()
}
Some(TOKEN_INTERPOL_START) => {
self.start_node(NODE_INTERPOL);
self.bump();
self.parse_expr();
self.expect(TOKEN_INTERPOL_END);
self.finish_node();
}
_ => break,
}
}
}
self.finish_node();
}
t if t.is_literal() => {
self.start_node(NODE_LITERAL);
self.bump();
self.finish_node();
}
TOKEN_IDENT => {
let ident = self.peek_raw().map(|&(_, s)| s);
match self.peek_ahead(1) {
Some(T![:]) => {
self.expect_ident();
self.start_node_at(checkpoint, NODE_LAMBDA);
self.start_node_at(checkpoint, NODE_IDENT_PARAM);
self.finish_node();
self.expect(T![:]);
self.parse_expr();
self.finish_node();
}
Some(T![@]) => {
self.expect_ident();
self.start_node_at(checkpoint, NODE_LAMBDA);
self.start_node_at(checkpoint, NODE_PATTERN);
self.start_node_at(checkpoint, NODE_PAT_BIND);
self.expect(T![@]);
self.finish_node(); // PatBind
self.expect(T!['{']);
self.parse_pattern(true);
self.finish_node(); // Pattern
self.expect(T![:]);
self.parse_expr();
self.finish_node(); // Lambda
}
_ if ident == Some("__curPos") => {
self.start_node(NODE_CUR_POS);
let (_, s) = self.try_next().unwrap();
self.manual_bump(s, TOKEN_CUR_POS);
self.finish_node();
}
_ => self.expect_ident(),
}
}
kind => {
let start = self.start_error_node();
self.bump();
let end = self.finish_error_node();
self.errors.push(ParseError::UnexpectedWanted(
kind,
TextRange::new(start, end),
[T!['('], T![rec], T!['{'], T!['['], TOKEN_STRING_START, TOKEN_IDENT]
.to_vec()
.into_boxed_slice(),
));
}
};
if self.peek() == Some(T![.]) {
self.start_node_at(checkpoint, NODE_SELECT);
self.bump();
self.parse_attrpath();
if self.peek() == Some(T![or]) {
self.bump();
self.parse_simple();
}
self.finish_node();
// This seems weird, but it matches Nix's behavior.
// If there is no "." but a "or" immediately followed a primary expression,
// we construct a application node, with "or" parsed as an identifier,
// ignoring the associativity of ancestor applications.
// Eg.
// "a b or c" => "((a (b or)) c)"
// "or a" => fail
} else if self.peek() == Some(T![or]) {
self.start_node_at(checkpoint, NODE_APPLY);
self.start_node(NODE_IDENT);
let (_, s) = self.try_next().unwrap();
self.manual_bump(s, TOKEN_IDENT);
self.finish_node();
self.finish_node();
}
checkpoint
}
fn parse_fn(&mut self) -> Checkpoint {
let checkpoint = self.parse_simple();
while self.peek().map(|t| t.is_fn_arg()).unwrap_or(false) {
self.start_node_at(checkpoint, NODE_APPLY);
self.parse_simple();
self.finish_node();
}
checkpoint
}
fn parse_negate(&mut self) -> Checkpoint {
if self.peek() == Some(T![-]) {
let checkpoint = self.checkpoint();
self.start_node(NODE_UNARY_OP);
self.bump();
self.parse_negate();
self.finish_node();
checkpoint
} else {
self.parse_fn()
}
}
fn parse_non_assoc(&mut self, next: fn(&mut Self) -> Checkpoint, ops: TokenSet) -> Checkpoint {
let checkpoint = next(self);
if self.peek().map(|t| ops.contains(t)).unwrap_or(false) {
self.start_node_at(checkpoint, NODE_BIN_OP);
self.bump();
next(self);
self.finish_node();
}
checkpoint
}
fn parse_left_assoc(&mut self, next: fn(&mut Self) -> Checkpoint, ops: TokenSet) -> Checkpoint {
let checkpoint = next(self);
while self.peek().map(|t| ops.contains(t)).unwrap_or(false) {
self.start_node_at(checkpoint, NODE_BIN_OP);
self.bump();
next(self);
self.finish_node();
}
checkpoint
}
fn parse_right_assoc(
&mut self,
next: fn(&mut Self) -> Checkpoint,
ops: TokenSet,
) -> Checkpoint {
let checkpoint = next(self);
if self.peek().map(|t| ops.contains(t)).unwrap_or(false) {
self.start_node_at(checkpoint, NODE_BIN_OP);
self.bump();
self.parse_right_assoc(next, ops);
self.finish_node();
}
checkpoint
}
fn parse_hasattr(&mut self) -> Checkpoint {
let checkpoint = self.parse_negate();
while self.peek().map(|t| t == T![?]).unwrap_or(false) {
self.start_node_at(checkpoint, NODE_HAS_ATTR);
self.bump();
self.parse_attrpath();
self.finish_node();
}
checkpoint
}
fn parse_concat(&mut self) -> Checkpoint {
self.parse_right_assoc(Self::parse_hasattr, T![++] | ())
}
fn parse_mul(&mut self) -> Checkpoint {
self.parse_left_assoc(Self::parse_concat, T![*] | T![/])
}
fn parse_add(&mut self) -> Checkpoint {
self.parse_left_assoc(Self::parse_mul, T![+] | T![-])
}
fn parse_invert(&mut self) -> Checkpoint {
if self.peek() == Some(TOKEN_INVERT) {
let checkpoint = self.checkpoint();
self.start_node(NODE_UNARY_OP);
self.bump();
self.parse_invert();
self.finish_node();
checkpoint
} else {
self.parse_add()
}
}
fn parse_merge(&mut self) -> Checkpoint {
self.parse_right_assoc(Self::parse_invert, T!["//"] | ())
}
fn parse_compare(&mut self) -> Checkpoint {
self.parse_non_assoc(Self::parse_merge, T![<] | T![<=] | T![>] | T![>=])
}
fn parse_equal(&mut self) -> Checkpoint {
self.parse_non_assoc(Self::parse_compare, T![==] | T![!=])
}
fn parse_and(&mut self) -> Checkpoint {
self.parse_left_assoc(Self::parse_equal, T![&&] | ())
}
fn parse_or(&mut self) -> Checkpoint {
self.parse_left_assoc(Self::parse_and, T![||] | ())
}
fn parse_implication(&mut self) -> Checkpoint {
self.parse_right_assoc(Self::parse_or, T![->] | ())
}
#[inline(always)]
fn parse_math(&mut self) -> Checkpoint {
// Always point this to the lowest-level math function there is
self.parse_implication()
}
fn parse_pipe_right(&mut self) -> Checkpoint {
self.parse_left_assoc(Self::parse_math, T!["|>"] | ())
}
fn parse_pipe_left(&mut self) -> Checkpoint {
self.parse_right_assoc(Self::parse_pipe_right, T!["<|"] | ())
}
/// Parse Nix code into an AST
pub fn parse_expr(&mut self) -> Checkpoint {
// Limit chosen somewhat arbitrarily
if self.depth >= 512 {
self.errors.push(ParseError::RecursionLimitExceeded);
// Consume tokens to the end of the file. Erroring without bumping might cause
// infinite looping elsewhere.
self.start_error_node();
while self.peek().is_some() {
self.bump()
}
self.finish_error_node();
return self.checkpoint();
}
self.depth += 1;
let out = match self.peek() {
Some(T![let]) => {
let checkpoint = self.checkpoint();
self.bump();
if self.peek() == Some(T!['{']) {
self.start_node_at(checkpoint, NODE_LEGACY_LET);
self.bump();
self.parse_set(T!['}']);
self.finish_node();
} else {
self.start_node_at(checkpoint, NODE_LET_IN);
self.parse_set(T![in]);
self.parse_expr();
self.finish_node();
}
checkpoint
}
Some(T![with]) => {
let checkpoint = self.checkpoint();
self.start_node(NODE_WITH);
self.bump();
self.parse_expr();
self.expect(T![;]);
self.parse_expr();
self.finish_node();
checkpoint
}
Some(T![if]) => {
let checkpoint = self.checkpoint();
self.start_node(NODE_IF_ELSE);
self.bump();
self.parse_expr();
self.expect(T![then]);
self.parse_expr();
self.expect(TOKEN_ELSE);
self.parse_expr();
self.finish_node();
checkpoint
}
Some(T![assert]) => {
let checkpoint = self.checkpoint();
self.start_node(NODE_ASSERT);
self.bump();
self.parse_expr();
self.expect(T![;]);
self.parse_expr();
self.finish_node();
checkpoint
}
_ => self.parse_pipe_left(),
};
self.depth -= 1;
out
}
}
/// Parse tokens into an AST
pub fn parse<'s, I>(iter: I) -> (GreenNode, Vec<ParseError>)
where
I: Iterator<Item = Token<'s>>,
{
let mut parser = Parser::new(iter);
parser.builder.start_node(NixLanguage::kind_to_raw(NODE_ROOT));
parser.parse_expr();
parser.eat_trivia();
if parser.peek().is_some() {
let start = parser.start_error_node();
while parser.peek().is_some() {
parser.bump();
}
let end = parser.finish_error_node();
parser.errors.push(ParseError::UnexpectedExtra(TextRange::new(start, end)));
parser.eat_trivia();
}
parser.builder.finish_node();
(parser.builder.finish(), parser.errors)
}
+127
View File
@@ -0,0 +1,127 @@
use std::{ffi::OsStr, fmt::Write, fs, path::PathBuf};
use expect_test::expect_file;
use rowan::ast::AstNode;
use crate::{
ast::{self, HasEntry},
tokenize, Root, SyntaxKind,
};
#[test]
fn interpolation() {
let root = ast::Root::parse(include_str!("../test_data/parser/success/interpolation.nix"))
.ok()
.unwrap();
let let_in = ast::LetIn::try_from(root.expr().unwrap()).unwrap();
let set = ast::AttrSet::try_from(let_in.body().unwrap()).unwrap();
let entry = set.entries().nth(1).unwrap();
let attrpath_value = ast::AttrpathValue::try_from(entry).unwrap();
let value = ast::Str::try_from(attrpath_value.value().unwrap()).unwrap();
match &*value.normalized_parts() {
&[
ast::InterpolPart::Literal(ref s1),
ast::InterpolPart::Interpolation(_),
ast::InterpolPart::Literal(ref s2),
ast::InterpolPart::Interpolation(_),
ast::InterpolPart::Literal(ref s3)
]
if s1 == "The set's x value is: "
&& s2 == "\n\nThis line shall have no indention\n This line shall be indented by 2\n\n\n"
&& s3 == "\n" => (),
parts => panic!("did not match: {:#?}", parts)
}
}
#[test]
fn inherit() {
let root =
ast::Root::parse(include_str!("../test_data/parser/success/inherit.nix")).ok().unwrap();
let let_in = ast::LetIn::try_from(root.expr().unwrap()).unwrap();
let set = ast::AttrSet::try_from(let_in.body().unwrap()).unwrap();
let inherit = set.inherits().nth(1).unwrap();
let from = inherit.from().unwrap().expr().unwrap();
let ident: ast::Ident = ast::Ident::try_from(from).unwrap();
assert_eq!(ident.syntax().text(), "set");
let mut children = inherit.attrs();
assert_eq!(children.next().unwrap().syntax().text(), "z");
assert_eq!(children.next().unwrap().syntax().text(), "a");
assert!(children.next().is_none());
}
#[test]
fn math() {
let root = ast::Root::parse(include_str!("../test_data/parser/success/math.nix")).ok().unwrap();
let op1 = ast::BinOp::try_from(root.expr().unwrap()).unwrap();
let op2 = ast::BinOp::try_from(op1.lhs().unwrap()).unwrap();
assert_eq!(op1.operator().unwrap(), ast::BinOpKind::Add);
let lhs = ast::Literal::try_from(op2.lhs().unwrap()).unwrap();
assert_eq!(lhs.syntax().text(), "1");
let rhs = ast::BinOp::try_from(op2.rhs().unwrap()).unwrap();
assert_eq!(rhs.operator().unwrap(), ast::BinOpKind::Mul);
}
#[test]
fn t_macro() {
assert_eq!(T![@], SyntaxKind::TOKEN_AT);
assert!(matches!(SyntaxKind::TOKEN_L_PAREN, T!['(']));
}
fn dir_tests<F>(dir: &str, get_actual: F)
where
F: Fn(String) -> String,
{
let base_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "test_data", dir].iter().collect();
let success_path = base_path.join("success");
let error_path = base_path.join("error");
let entries = success_path.read_dir().unwrap().chain(error_path.read_dir().unwrap());
for entry in entries {
let path = entry.unwrap().path();
if path.extension() != Some(OsStr::new("nix")) {
continue;
}
println!("testing: {}", path.display());
let mut code = fs::read_to_string(&path).unwrap();
if code.ends_with('\n') {
code.truncate(code.len() - 1);
}
let actual = get_actual(code);
expect_file![path.with_extension("expect")].assert_eq(&actual);
}
}
#[test]
fn parser_dir_tests() {
dir_tests("parser", |code| {
let parse = Root::parse(&code);
let mut actual = String::new();
for error in parse.errors() {
writeln!(actual, "error: {}", error).unwrap();
}
writeln!(actual, "{:#?}", parse.syntax()).unwrap();
actual
})
}
#[test]
fn tokenizer_dir_tests() {
dir_tests("tokenizer", |code| {
let mut actual = String::new();
for (kind, str) in tokenize(&code) {
writeln!(actual, "{:?}, \"{}\"", kind, str).unwrap();
}
actual
})
}
+86
View File
@@ -0,0 +1,86 @@
use std::ops;
use crate::SyntaxKind;
/// A bit-set of `SyntaxKind`s
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TokenSet(u128);
impl TokenSet {
#[allow(dead_code)]
pub(crate) const EMPTY: TokenSet = TokenSet(0);
pub(crate) const fn new(kind: SyntaxKind) -> TokenSet {
TokenSet(mask(kind))
}
pub(crate) const fn from_slice(kinds: &[SyntaxKind]) -> TokenSet {
let mut res = 0u128;
let mut i = 0;
while i < kinds.len() {
res |= mask(kinds[i]);
i += 1
}
TokenSet(res)
}
pub(crate) const fn union(self, other: TokenSet) -> TokenSet {
TokenSet(self.0 | other.0)
}
pub(crate) const fn contains(&self, kind: SyntaxKind) -> bool {
self.0 & mask(kind) != 0
}
}
const fn mask(kind: SyntaxKind) -> u128 {
1u128 << (kind as usize)
}
impl ops::BitOr for SyntaxKind {
type Output = TokenSet;
fn bitor(self, rhs: Self) -> Self::Output {
TokenSet(mask(self) | mask(rhs))
}
}
impl ops::BitOr<SyntaxKind> for TokenSet {
type Output = TokenSet;
fn bitor(self, rhs: SyntaxKind) -> Self::Output {
self.union(TokenSet(mask(rhs)))
}
}
impl ops::BitOr<TokenSet> for SyntaxKind {
type Output = TokenSet;
fn bitor(self, rhs: TokenSet) -> Self::Output {
TokenSet(mask(self)).union(rhs)
}
}
impl ops::BitOr<TokenSet> for TokenSet {
type Output = TokenSet;
fn bitor(self, rhs: TokenSet) -> Self::Output {
self.union(rhs)
}
}
impl ops::BitOr<SyntaxKind> for () {
type Output = TokenSet;
fn bitor(self, rhs: SyntaxKind) -> Self::Output {
TokenSet::new(rhs)
}
}
impl ops::BitOr<()> for SyntaxKind {
type Output = TokenSet;
fn bitor(self, (): ()) -> Self::Output {
TokenSet::new(self)
}
}
+566
View File
@@ -0,0 +1,566 @@
//! The tokenizer: turns a string into tokens, such as numbers, strings, and keywords
use crate::SyntaxKind::{self, *};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum IdentType {
Ident,
PathRel,
PathAbs,
PathHome,
PathSearch,
Uri,
}
fn is_valid_path_char(c: char) -> bool {
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '/' | '_' | '.' | '+' | '-')
}
fn is_valid_uri_char(c: char) -> bool {
match c {
'%' | '?' | ':' | '@' | '&' | '=' | '$' | ',' | '!' | '~' | '*' | '\'' => true,
_ => is_valid_path_char(c),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Context {
StringBody { multiline: bool },
StringEnd,
Interpol { brackets: u32 },
InterpolStart,
Path(SyntaxKind),
}
#[derive(Clone, Copy)]
struct State<'a> {
input: &'a str,
offset: usize,
}
impl PartialEq for State<'_> {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.input, other.input) && self.offset == other.offset
}
}
impl Eq for State<'_> {}
pub type Token<'a> = (SyntaxKind, &'a str);
/// Tokenize the given input
pub fn tokenize(input: &str) -> impl Iterator<Item = Token<'_>> + '_ {
Tokenizer::new(input)
}
struct Tokenizer<'a> {
ctx: Vec<Context>,
state: State<'a>,
}
impl<'a> Tokenizer<'a> {
pub fn new(input: &'a str) -> Self {
Self { ctx: Vec::new(), state: State { input, offset: 0 } }
}
}
impl Tokenizer<'_> {
fn remaining(&self) -> &str {
&self.state.input[self.state.offset..]
}
fn peek(&self) -> Option<char> {
self.remaining().chars().next()
}
fn next(&mut self) -> Option<char> {
let c = self.peek();
if let Some(c) = c {
self.state.offset += c.len_utf8();
}
c
}
fn starts_with_bump(&mut self, s: &str) -> bool {
let starts_with = self.remaining().starts_with(s);
if starts_with {
self.state.offset += s.len();
}
starts_with
}
fn str_since<'a>(&self, past: State<'a>) -> &'a str {
&past.input[past.offset..self.state.offset]
}
fn push_ctx(&mut self, ctx: Context) {
self.ctx.push(ctx)
}
fn pop_ctx(&mut self, ctx: Context) {
debug_assert!(self
.ctx
.last()
.is_some_and(|c| std::mem::discriminant(c) == std::mem::discriminant(&ctx)));
self.ctx.pop();
}
/// Consumes all next characters.
/// while the predicate function [f] returns true
///
/// returns the count of consumed characters.
fn consume<F>(&mut self, mut f: F) -> usize
where
F: FnMut(char) -> bool,
{
let len: usize =
self.remaining().chars().take_while(|&c| f(c)).map(|c| c.len_utf8()).sum::<usize>();
self.state.offset += len;
len
}
fn next_string(&mut self, multiline: bool) -> SyntaxKind {
loop {
let start = self.state;
match self.next() {
None => {
self.pop_ctx(Context::StringBody { multiline });
return TOKEN_ERROR;
}
Some('"') if !multiline => {
self.state = start;
self.pop_ctx(Context::StringBody { multiline: false });
self.push_ctx(Context::StringEnd);
return TOKEN_STRING_CONTENT;
}
Some('\\') if !multiline => {
if self.next().is_none() {
return TOKEN_ERROR;
}
}
Some('\'') if multiline => match self.peek() {
None => return TOKEN_ERROR,
Some('\'') => {
self.next();
match self.peek() {
Some('\'') | Some('$') => {
self.next().unwrap();
}
Some('\\') => {
self.next().unwrap();
if self.next().is_none() {
return TOKEN_ERROR;
}
}
_ => {
self.state = start;
self.pop_ctx(Context::StringBody { multiline: true });
self.push_ctx(Context::StringEnd);
return TOKEN_STRING_CONTENT;
}
}
}
Some(_) => (),
},
Some('$') => match self.peek() {
Some('$') => {
self.next().unwrap();
}
Some('{') => {
self.state = start;
self.push_ctx(Context::InterpolStart);
return TOKEN_STRING_CONTENT;
}
_ => (),
},
Some(_) => (),
}
}
}
fn check_path_since(&mut self, past: State, kind: SyntaxKind) -> SyntaxKind {
self.consume(is_valid_path_char);
let path_str = self.str_since(past);
if self.remaining().starts_with("${") {
self.ctx.push(Context::InterpolStart);
} else if path_str.ends_with('/') || path_str.contains("//") {
return TOKEN_ERROR;
} else {
self.pop_ctx(Context::Path(kind));
}
kind
}
fn next_inner(&mut self) -> Option<SyntaxKind> {
let start = self.state;
// Handle already started multi-token
loop {
match self.ctx.last() {
Some(Context::InterpolStart) => {
self.pop_ctx(Context::InterpolStart);
self.ctx.push(Context::Interpol { brackets: 0 });
if self.starts_with_bump("${") {
return Some(TOKEN_INTERPOL_START);
} else {
unreachable!()
}
}
Some(Context::Path(path_kind)) => {
let path_kind = *path_kind;
// Search paths (<nixpkgs>) don't support interpolation
if path_kind == TOKEN_PATH_SEARCH {
if self.peek().is_some_and(is_valid_path_char) {
return Some(self.check_path_since(start, path_kind));
} else {
self.pop_ctx(Context::Path(path_kind));
}
} else if self.starts_with_bump("${") {
self.ctx.push(Context::Interpol { brackets: 0 });
return Some(TOKEN_INTERPOL_START);
} else if self.peek().is_some_and(is_valid_path_char) {
return Some(self.check_path_since(start, path_kind));
} else {
self.pop_ctx(Context::Path(path_kind));
}
}
Some(&Context::StringBody { multiline }) => {
let token = self.next_string(multiline);
// skip empty stuff
if self.state == start {
continue;
}
return Some(token);
}
Some(Context::StringEnd) => {
self.pop_ctx(Context::StringEnd);
let status = match self.peek() {
Some('"') => {
self.next().unwrap();
true
}
Some('\'') => {
self.next().unwrap();
match self.peek() {
Some('\'') => {
self.next().unwrap();
true
}
_ => false,
}
}
_ => false,
};
return Some(if status { TOKEN_STRING_END } else { TOKEN_ERROR });
}
_ => (),
}
break;
}
if self.consume(char::is_whitespace) > 0 {
return Some(TOKEN_WHITESPACE);
}
if self.peek() == Some('#') {
self.consume(|c| c != '\n');
return Some(TOKEN_COMMENT);
}
if self.starts_with_bump("/*") {
loop {
self.consume(|c| c != '*');
self.next(); // consume the '*', if any
match self.peek() {
None => return Some(TOKEN_ERROR),
Some('/') => {
self.next().unwrap();
return Some(TOKEN_COMMENT);
}
_ => (),
}
}
}
if self.starts_with_bump("...") {
return Some(TOKEN_ELLIPSIS);
}
// Check if it's a path
// First, simple prefix based detection for absolute ("/...") or home relative ("~/...") paths.
// For absolute paths, we need to be careful to not interfere with operators
let kind = if self.peek() == Some('/') {
// For absolute paths, we need to have a valid path character after the '/'
// AND we need to make sure it's not an operator like //
if self.remaining().starts_with("//") {
None // This could be an update operator, let the operator matcher handle it
} else if self.remaining().starts_with("/${") {
// Absolute path with interpolation, e.g. /${foo}/bar
Some(IdentType::PathAbs)
} else {
let second_char = self.remaining().chars().nth(1);
if second_char.is_some_and(|c| is_valid_path_char(c) && c != '*') {
Some(IdentType::PathAbs)
} else {
None // Not a path, might be division operator or something else
}
}
} else if self.peek() == Some('~') && self.remaining().chars().nth(1) == Some('/') {
Some(IdentType::PathHome)
} else {
// Fallback to heuristic previously used for relative, search and URI paths.
let store_path = self.peek() == Some('<');
let skipped = self
.remaining()
.chars()
.take_while(|&c| match c {
'<' | '/' => store_path,
_ => is_valid_path_char(c),
})
.collect::<String>();
let mut lookahead = self.remaining().chars().skip(skipped.chars().count());
match (lookahead.next(), lookahead.next()) {
// a//b parses as Update(a, b)
(Some('/'), Some('/')) => None,
(Some('/'), Some('*')) => None,
(Some('/'), Some(c)) if !c.is_whitespace() => {
// If the first character of the yet‑to‑be‑consumed token was '/', we treat it as absolute later.
// Otherwise it's a relative path.
Some(IdentType::PathRel)
}
(Some('>'), _) => Some(IdentType::PathSearch),
(Some(':'), Some(c)) if is_valid_uri_char(c) && !skipped.contains('_') => {
Some(IdentType::Uri)
}
_ => None,
}
};
let c = self.next()?;
// First, handle specific operators that might be confused with paths
if c == '/' && self.peek() == Some('/') {
// This is the update operator '//'
self.next().unwrap(); // Consume the second '/'
return Some(TOKEN_UPDATE);
}
if c == '~'
|| matches!(kind, Some(IdentType::PathAbs | IdentType::PathRel))
|| (c == '/' && self.peek().is_some_and(is_valid_path_char))
{
return Some({
// Determine the concrete path token kind to use
let token_kind = if c == '~' {
// we've just consumed '~', ensure '/' exists already consumed by next()
if self.next() != Some('/') {
return Some(TOKEN_ERROR);
}
TOKEN_PATH_HOME
} else {
// For abs vs rel: If the first char we consumed was '/', it's absolute.
if c == '/' {
TOKEN_PATH_ABS
} else {
TOKEN_PATH_REL
}
};
// Store the kind in context so subsequent segments are consistent
self.push_ctx(Context::Path(token_kind));
self.check_path_since(start, token_kind)
});
}
Some(match c {
'=' if self.peek() == Some('=') => {
self.next().unwrap();
TOKEN_EQUAL
}
'!' if self.peek() == Some('=') => {
self.next().unwrap();
TOKEN_NOT_EQUAL
}
'!' => TOKEN_INVERT,
'{' => {
if let Some(Context::Interpol { brackets }) = self.ctx.last_mut() {
*brackets += 1;
}
TOKEN_L_BRACE
}
'}' => {
if let Some(Context::Interpol { brackets }) = self.ctx.last_mut() {
match brackets.checked_sub(1) {
Some(new) => *brackets = new,
None => {
self.pop_ctx(Context::Interpol { brackets: 0 });
return Some(TOKEN_INTERPOL_END);
}
}
}
TOKEN_R_BRACE
}
'[' => TOKEN_L_BRACK,
']' => TOKEN_R_BRACK,
'@' => TOKEN_AT,
':' => TOKEN_COLON,
',' => TOKEN_COMMA,
'.' => {
if self.peek().is_some_and(|x| x.is_ascii_digit()) {
self.consume(|c| c.is_ascii_digit());
self.consume_scientific()
} else {
TOKEN_DOT
}
}
'=' => TOKEN_ASSIGN,
'?' => TOKEN_QUESTION,
';' => TOKEN_SEMICOLON,
'(' => TOKEN_L_PAREN,
')' => TOKEN_R_PAREN,
'+' if self.peek() == Some('+') => {
self.next().unwrap();
TOKEN_CONCAT
}
'-' if self.peek() == Some('>') => {
self.next().unwrap();
TOKEN_IMPLICATION
}
'/' if self.peek() == Some('/') => {
self.next().unwrap();
TOKEN_UPDATE
}
'+' => TOKEN_ADD,
'-' => TOKEN_SUB,
'*' => TOKEN_MUL,
'/' => TOKEN_DIV,
'<' if self.peek() == Some('|') => {
self.next().unwrap();
TOKEN_PIPE_LEFT
}
'<' if kind == Some(IdentType::PathSearch) => {
let content_start = self.state.offset;
self.consume(is_valid_path_char);
// Check if we consumed any content between < and >
if self.state.offset == content_start {
// Empty search path <> is not valid
TOKEN_LESS
} else if self.next() != Some('>') {
TOKEN_ERROR
} else {
TOKEN_PATH_SEARCH
}
}
'&' if self.peek() == Some('&') => {
self.next().unwrap();
TOKEN_AND_AND
}
'|' if self.peek() == Some('>') => {
self.next().unwrap();
TOKEN_PIPE_RIGHT
}
'|' if self.peek() == Some('|') => {
self.next().unwrap();
TOKEN_OR_OR
}
'<' if self.peek() == Some('=') => {
self.next().unwrap();
TOKEN_LESS_OR_EQ
}
'<' => TOKEN_LESS,
'>' if self.peek() == Some('=') => {
self.next().unwrap();
TOKEN_MORE_OR_EQ
}
'>' => TOKEN_MORE,
'$' if self.peek() == Some('{') => {
self.next().unwrap();
self.push_ctx(Context::Interpol { brackets: 0 });
TOKEN_INTERPOL_START
}
'a'..='z' | 'A'..='Z' | '_' => {
let kind = match kind {
// It's detected as store if it ends with >, but if it
// didn't start with <, that's wrong
Some(IdentType::PathSearch) | None => IdentType::Ident,
Some(kind) => kind,
};
self.consume(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\'' => true,
c => kind == IdentType::Uri && is_valid_uri_char(c),
});
match kind {
IdentType::Ident => match self.str_since(start) {
"assert" => TOKEN_ASSERT,
"else" => TOKEN_ELSE,
"if" => TOKEN_IF,
"in" => TOKEN_IN,
"inherit" => TOKEN_INHERIT,
"let" => TOKEN_LET,
// "or" is a contextual keyword and will be handled in the parser.
"or" => TOKEN_OR,
"rec" => TOKEN_REC,
"then" => TOKEN_THEN,
"with" => TOKEN_WITH,
_ => TOKEN_IDENT,
},
IdentType::Uri => TOKEN_URI,
IdentType::PathAbs | IdentType::PathRel | IdentType::PathHome => {
panic!("paths are checked earlier")
}
IdentType::PathSearch => panic!("search paths are checked earlier"),
}
}
'"' => {
self.push_ctx(Context::StringBody { multiline: false });
TOKEN_STRING_START
}
'\'' if self.peek() == Some('\'') => {
self.next().unwrap();
self.push_ctx(Context::StringBody { multiline: true });
TOKEN_STRING_START
}
fst @ '0'..='9' => {
// Due to nix's behavior on leading 0's we know immediately
// That a '0' followed by some other number character MUST tokenize into an INT.
// More background on this: https://github.com/nix-community/rnix-parser/issues/157
if fst == '0' && self.peek() != Some('.') {
// In that case just consume the remaining number characters to fill the TOKEN_INTEGER
self.consume(|c| c.is_ascii_digit());
TOKEN_INTEGER
} else {
// If we are not in that edge-case we can just continue with the regular number tokenization
self.consume(|c| c.is_ascii_digit());
if self.peek() == Some('.') {
self.next().unwrap();
self.consume(|c| c.is_ascii_digit());
self.consume_scientific()
} else {
TOKEN_INTEGER
}
}
}
_ => TOKEN_ERROR,
})
}
fn consume_scientific(&mut self) -> SyntaxKind {
if self.peek() == Some('e') || self.peek() == Some('E') {
self.next().unwrap();
if self.peek() == Some('-') || self.peek() == Some('+') {
self.next().unwrap();
}
if self.consume(|c| c.is_ascii_digit()) == 0 {
return TOKEN_ERROR;
}
}
TOKEN_FLOAT
}
}
impl<'a> Iterator for Tokenizer<'a> {
type Item = Token<'a>;
fn next(&mut self) -> Option<Self::Item> {
let start = self.state;
self.next_inner().map(|syntax_kind| (syntax_kind, self.str_since(start)))
}
}
@@ -0,0 +1,21 @@
error: unexpected TOKEN_ERROR at 2..3, wanted any of [TOKEN_IDENT, TOKEN_OR, TOKEN_CUR_POS]
NODE_ROOT@0..12
NODE_ATTR_SET@0..12
TOKEN_L_BRACE@0..1 "{"
TOKEN_WHITESPACE@1..2 " "
NODE_ATTRPATH_VALUE@2..10
NODE_ATTRPATH@2..4
NODE_ERROR@2..3
TOKEN_ERROR@2..3 "$"
NODE_IDENT@3..4
TOKEN_IDENT@3..4 "a"
TOKEN_WHITESPACE@4..5 " "
TOKEN_ASSIGN@5..6 "="
TOKEN_WHITESPACE@6..7 " "
NODE_STRING@7..9
TOKEN_STRING_START@7..8 "\""
TOKEN_STRING_END@8..9 "\""
TOKEN_SEMICOLON@9..10 ";"
TOKEN_WHITESPACE@10..11 " "
TOKEN_R_BRACE@11..12 "}"
@@ -0,0 +1 @@
{ $a = ""; }
@@ -0,0 +1,110 @@
error: unexpected TOKEN_STRING_START at 10..28, wanted any of [TOKEN_ASSIGN]
error: unexpected TOKEN_STRING_START at 78..98, wanted any of [TOKEN_ASSIGN]
error: unexpected TOKEN_R_BRACE at 162..166, wanted any of [TOKEN_ASSIGN]
error: unexpected end of file, wanted any of [TOKEN_ASSIGN]
error: unexpected end of file
error: unexpected end of file, wanted any of [TOKEN_SEMICOLON]
error: unexpected end of file
error: unexpected end of file, wanted any of [TOKEN_SEMICOLON]
error: unexpected end of file
NODE_ROOT@0..166
NODE_ATTR_SET@0..166
TOKEN_L_BRACE@0..1 "{"
TOKEN_WHITESPACE@1..4 "\n "
NODE_ATTRPATH_VALUE@4..33
NODE_ATTRPATH@4..9
NODE_IDENT@4..9
TOKEN_IDENT@4..9 "hello"
TOKEN_WHITESPACE@9..10 " "
NODE_ERROR@10..28
TOKEN_STRING_START@10..11 "\""
TOKEN_STRING_CONTENT@11..27 "Hello Ùnicödə"
TOKEN_STRING_END@27..28 "\""
TOKEN_WHITESPACE@28..29 " "
TOKEN_ASSIGN@29..30 "="
TOKEN_WHITESPACE@30..31 " "
NODE_LITERAL@31..32
TOKEN_INTEGER@31..32 "1"
TOKEN_SEMICOLON@32..33 ";"
TOKEN_WHITESPACE@33..36 "\n "
NODE_ATTRPATH_VALUE@36..120
NODE_ATTRPATH@36..40
NODE_IDENT@36..40
TOKEN_IDENT@36..40 "test"
TOKEN_WHITESPACE@40..41 " "
TOKEN_ASSIGN@41..42 "="
TOKEN_WHITESPACE@42..43 " "
NODE_ATTR_SET@43..119
TOKEN_L_BRACE@43..44 "{"
TOKEN_WHITESPACE@44..49 "\n "
NODE_ATTRPATH_VALUE@49..65
NODE_ATTRPATH@49..54
NODE_IDENT@49..54
TOKEN_IDENT@49..54 "valid"
TOKEN_WHITESPACE@54..55 " "
TOKEN_ASSIGN@55..56 "="
TOKEN_WHITESPACE@56..57 " "
NODE_STRING@57..64
TOKEN_STRING_START@57..58 "\""
TOKEN_STRING_CONTENT@58..63 "entry"
TOKEN_STRING_END@63..64 "\""
TOKEN_SEMICOLON@64..65 ";"
TOKEN_WHITESPACE@65..70 "\n "
NODE_ATTRPATH_VALUE@70..115
NODE_ATTRPATH@70..77
NODE_IDENT@70..77
TOKEN_IDENT@70..77 "invalid"
TOKEN_WHITESPACE@77..78 " "
NODE_ERROR@78..98
TOKEN_STRING_START@78..79 "\""
TOKEN_STRING_CONTENT@79..84 "entry"
TOKEN_STRING_END@84..85 "\""
TOKEN_SEMICOLON@85..86 ";"
TOKEN_WHITESPACE@86..91 "\n "
TOKEN_IDENT@91..98 "another"
TOKEN_WHITESPACE@98..99 " "
TOKEN_ASSIGN@99..100 "="
TOKEN_WHITESPACE@100..101 " "
NODE_STRING@101..114
TOKEN_STRING_START@101..102 "\""
TOKEN_STRING_CONTENT@102..113 "valid entry"
TOKEN_STRING_END@113..114 "\""
TOKEN_SEMICOLON@114..115 ";"
TOKEN_WHITESPACE@115..118 "\n "
TOKEN_R_BRACE@118..119 "}"
TOKEN_SEMICOLON@119..120 ";"
TOKEN_WHITESPACE@120..123 "\n "
NODE_ATTRPATH_VALUE@123..166
NODE_ATTRPATH@123..128
NODE_IDENT@123..128
TOKEN_IDENT@123..128 "test2"
TOKEN_WHITESPACE@128..129 " "
TOKEN_ASSIGN@129..130 "="
TOKEN_WHITESPACE@130..131 " "
NODE_ATTR_SET@131..166
TOKEN_L_BRACE@131..132 "{"
TOKEN_WHITESPACE@132..137 "\n "
NODE_ATTRPATH_VALUE@137..150
NODE_ATTRPATH@137..142
NODE_IDENT@137..142
TOKEN_IDENT@137..142 "hello"
TOKEN_WHITESPACE@142..143 " "
TOKEN_ASSIGN@143..144 "="
TOKEN_WHITESPACE@144..145 " "
NODE_STRING@145..149
TOKEN_STRING_START@145..146 "\""
TOKEN_STRING_CONTENT@146..148 "hi"
TOKEN_STRING_END@148..149 "\""
TOKEN_SEMICOLON@149..150 ";"
TOKEN_WHITESPACE@150..155 "\n "
NODE_ATTRPATH_VALUE@155..166
NODE_ATTRPATH@155..159
NODE_IDENT@155..159
TOKEN_IDENT@155..159 "aaaa"
TOKEN_WHITESPACE@159..162 "\n "
NODE_ERROR@162..166
TOKEN_R_BRACE@162..163 "}"
TOKEN_SEMICOLON@163..164 ";"
TOKEN_WHITESPACE@164..165 "\n"
TOKEN_R_BRACE@165..166 "}"
@@ -0,0 +1,12 @@
{
hello "Hello Ùnicödə" = 1;
test = {
valid = "entry";
invalid "entry";
another = "valid entry";
};
test2 = {
hello = "hi";
aaaa
};
}
@@ -0,0 +1,9 @@
error: unexpected token at 3..4
NODE_ROOT@0..4
NODE_ATTR_SET@0..2
TOKEN_L_BRACE@0..1 "{"
TOKEN_R_BRACE@1..2 "}"
TOKEN_WHITESPACE@2..3 "\n"
NODE_ERROR@3..4
TOKEN_COMMA@3..4 ","
@@ -0,0 +1,2 @@
{}
,
@@ -0,0 +1,20 @@
error: unexpected double bind at 4..6
NODE_ROOT@0..10
NODE_LAMBDA@0..10
NODE_PATTERN@0..6
NODE_PAT_BIND@0..2
NODE_IDENT@0..1
TOKEN_IDENT@0..1 "f"
TOKEN_AT@1..2 "@"
TOKEN_L_BRACE@2..3 "{"
TOKEN_R_BRACE@3..4 "}"
NODE_ERROR@4..6
TOKEN_AT@4..5 "@"
NODE_IDENT@5..6
TOKEN_IDENT@5..6 "f"
TOKEN_COLON@6..7 ":"
TOKEN_WHITESPACE@7..8 "\n"
NODE_STRING@8..10
TOKEN_STRING_START@8..9 "\""
TOKEN_STRING_END@9..10 "\""
@@ -0,0 +1,2 @@
f@{}@f:
""
@@ -0,0 +1,25 @@
error: unexpected TOKEN_L_PAREN at 12..13, wanted any of [TOKEN_IDENT, TOKEN_OR, TOKEN_CUR_POS]
error: unexpected TOKEN_R_PAREN at 14..15, wanted any of [TOKEN_IDENT, TOKEN_OR, TOKEN_CUR_POS]
NODE_ROOT@0..20
NODE_ATTR_SET@0..20
TOKEN_L_BRACE@0..1 "{"
TOKEN_WHITESPACE@1..2 " "
NODE_INHERIT@2..18
TOKEN_INHERIT@2..9 "inherit"
TOKEN_WHITESPACE@9..10 " "
NODE_IDENT@10..11
TOKEN_IDENT@10..11 "a"
TOKEN_WHITESPACE@11..12 " "
NODE_ERROR@12..13
TOKEN_L_PAREN@12..13 "("
NODE_IDENT@13..14
TOKEN_IDENT@13..14 "b"
NODE_ERROR@14..15
TOKEN_R_PAREN@14..15 ")"
TOKEN_WHITESPACE@15..16 " "
NODE_IDENT@16..17
TOKEN_IDENT@16..17 "c"
TOKEN_SEMICOLON@17..18 ";"
TOKEN_WHITESPACE@18..19 " "
TOKEN_R_BRACE@19..20 "}"
@@ -0,0 +1 @@
{ inherit a (b) c; }
@@ -0,0 +1,11 @@
error: unexpected end of file
error: unexpected end of file, wanted any of [TOKEN_SEMICOLON]
error: unexpected end of file
error: unexpected end of file
NODE_ROOT@0..13
NODE_LET_IN@0..13
TOKEN_LET@0..3 "let"
TOKEN_WHITESPACE@3..6 "\n "
NODE_INHERIT@6..13
TOKEN_INHERIT@6..13 "inherit"
@@ -0,0 +1,2 @@
let
inherit
@@ -0,0 +1,5 @@
error: unexpected TOKEN_ERROR at 0..1, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
NODE_ROOT@0..1
NODE_ERROR@0..1
TOKEN_ERROR@0..1 "~"
@@ -0,0 +1 @@
~
@@ -0,0 +1,5 @@
error: unexpected TOKEN_ERROR at 0..9, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
NODE_ROOT@0..9
NODE_ERROR@0..9
TOKEN_ERROR@0..9 "/foo//bar"
@@ -0,0 +1 @@
/foo//bar
@@ -0,0 +1,10 @@
error: unexpected token at 1..7
NODE_ROOT@0..7
NODE_IDENT@0..1
TOKEN_IDENT@0..1 "a"
NODE_ERROR@1..7
TOKEN_INTERPOL_START@1..3 "${"
TOKEN_IDENT@3..4 "b"
TOKEN_INTERPOL_END@4..5 "}"
TOKEN_PATH_ABS@5..7 "/c"
@@ -0,0 +1 @@
a${b}/c
@@ -0,0 +1,12 @@
error: unexpected token at 8..9
NODE_ROOT@0..9
NODE_PATH_REL@0..8
TOKEN_PATH_REL@0..2 "./"
NODE_INTERPOL@2..8
TOKEN_INTERPOL_START@2..4 "${"
NODE_IDENT@4..7
TOKEN_IDENT@4..7 "foo"
TOKEN_INTERPOL_END@7..8 "}"
NODE_ERROR@8..9
TOKEN_ERROR@8..9 "/"
@@ -0,0 +1 @@
./${foo}/
@@ -0,0 +1,59 @@
error: unexpected TOKEN_ERROR at 57..66, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
error: unexpected TOKEN_ERROR at 84..93, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
error: unexpected TOKEN_ERROR at 108..110, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
error: unexpected TOKEN_ERROR at 128..133, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
NODE_ROOT@0..141
NODE_ATTR_SET@0..141
TOKEN_L_BRACE@0..1 "{"
TOKEN_WHITESPACE@1..4 "\n "
TOKEN_COMMENT@4..38 "# Multiple path error ..."
TOKEN_WHITESPACE@38..41 "\n "
NODE_ATTRPATH_VALUE@41..67
NODE_ATTRPATH@41..54
NODE_IDENT@41..54
TOKEN_IDENT@41..54 "trailingSlash"
TOKEN_WHITESPACE@54..55 " "
TOKEN_ASSIGN@55..56 "="
TOKEN_WHITESPACE@56..57 " "
NODE_ERROR@57..66
TOKEN_ERROR@57..66 "/foo/bar/"
TOKEN_SEMICOLON@66..67 ";"
TOKEN_WHITESPACE@67..70 "\n "
NODE_ATTRPATH_VALUE@70..94
NODE_ATTRPATH@70..81
NODE_IDENT@70..81
TOKEN_IDENT@70..81 "doubleSlash"
TOKEN_WHITESPACE@81..82 " "
TOKEN_ASSIGN@82..83 "="
TOKEN_WHITESPACE@83..84 " "
NODE_ERROR@84..93
TOKEN_ERROR@84..93 "/foo//bar"
TOKEN_SEMICOLON@93..94 ";"
TOKEN_WHITESPACE@94..97 "\n "
NODE_ATTRPATH_VALUE@97..111
NODE_ATTRPATH@97..105
NODE_IDENT@97..105
TOKEN_IDENT@97..105 "bareHome"
TOKEN_WHITESPACE@105..106 " "
TOKEN_ASSIGN@106..107 "="
TOKEN_WHITESPACE@107..108 " "
NODE_ERROR@108..110
TOKEN_ERROR@108..110 "~/"
TOKEN_SEMICOLON@110..111 ";"
TOKEN_WHITESPACE@111..114 "\n "
NODE_ATTRPATH_VALUE@114..139
NODE_ATTRPATH@114..125
NODE_IDENT@114..125
TOKEN_IDENT@114..125 "tildeMiddle"
TOKEN_WHITESPACE@125..126 " "
TOKEN_ASSIGN@126..127 "="
TOKEN_WHITESPACE@127..128 " "
NODE_APPLY@128..138
NODE_ERROR@128..133
TOKEN_ERROR@128..133 "/foo/"
NODE_PATH_HOME@133..138
TOKEN_PATH_HOME@133..138 "~/bar"
TOKEN_SEMICOLON@138..139 ";"
TOKEN_WHITESPACE@139..140 "\n"
TOKEN_R_BRACE@140..141 "}"
@@ -0,0 +1,7 @@
{
# Multiple path errors in one file
trailingSlash = /foo/bar/;
doubleSlash = /foo//bar;
bareHome = ~/;
tildeMiddle = /foo/~/bar;
}
@@ -0,0 +1,17 @@
error: unexpected TOKEN_LESS at 0..1, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
error: unexpected end of file
NODE_ROOT@0..20
NODE_BIN_OP@0..20
NODE_APPLY@0..19
NODE_ERROR@0..1
TOKEN_LESS@0..1 "<"
NODE_PATH_REL@1..19
TOKEN_PATH_REL@1..9 "nixpkgs/"
NODE_INTERPOL@9..15
TOKEN_INTERPOL_START@9..11 "${"
NODE_IDENT@11..14
TOKEN_IDENT@11..14 "foo"
TOKEN_INTERPOL_END@14..15 "}"
TOKEN_PATH_REL@15..19 "/bar"
TOKEN_MORE@19..20 ">"
@@ -0,0 +1 @@
<nixpkgs/${foo}/bar>
@@ -0,0 +1,5 @@
error: unexpected TOKEN_ERROR at 0..2, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
NODE_ROOT@0..2
NODE_ERROR@0..2
TOKEN_ERROR@0..2 "~p"
@@ -0,0 +1 @@
~p
@@ -0,0 +1,5 @@
error: unexpected TOKEN_ERROR at 0..2, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
NODE_ROOT@0..2
NODE_ERROR@0..2
TOKEN_ERROR@0..2 "~/"
@@ -0,0 +1 @@
~/
@@ -0,0 +1,5 @@
error: unexpected TOKEN_ERROR at 0..3, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
NODE_ROOT@0..3
NODE_ERROR@0..3
TOKEN_ERROR@0..3 "/a/"
@@ -0,0 +1 @@
/a/
@@ -0,0 +1,9 @@
error: unexpected TOKEN_R_BRACK at 0..1, wanted any of [TOKEN_L_PAREN, TOKEN_REC, TOKEN_L_BRACE, TOKEN_L_BRACK, TOKEN_STRING_START, TOKEN_IDENT]
error: unexpected end of file, wanted any of [TOKEN_IDENT, TOKEN_OR, TOKEN_CUR_POS]
NODE_ROOT@0..2
NODE_SELECT@0..2
NODE_ERROR@0..1
TOKEN_R_BRACK@0..1 "]"
TOKEN_DOT@1..2 "."
NODE_ATTRPATH@2..2
@@ -0,0 +1 @@
].
@@ -0,0 +1,23 @@
error: unexpected end of file
error: unexpected end of file, wanted any of [TOKEN_SEMICOLON]
error: unexpected end of file
NODE_ROOT@0..50
NODE_ATTR_SET@0..48
TOKEN_L_BRACE@0..1 "{"
TOKEN_WHITESPACE@1..4 "\n "
NODE_ATTRPATH_VALUE@4..48
NODE_ATTRPATH@4..11
NODE_IDENT@4..11
TOKEN_IDENT@4..11 "traceIf"
TOKEN_WHITESPACE@11..12 " "
TOKEN_ASSIGN@12..13 "="
TOKEN_WHITESPACE@13..18 "\n "
TOKEN_COMMENT@18..38 "# predicate to check"
TOKEN_WHITESPACE@38..43 "\n "
NODE_LAMBDA@43..48
NODE_IDENT_PARAM@43..47
NODE_IDENT@43..47
TOKEN_IDENT@43..47 "pred"
TOKEN_COLON@47..48 ":"
TOKEN_WHITESPACE@48..50 "\n\n"
@@ -0,0 +1,6 @@
{
traceIf =
# predicate to check
pred:
@@ -0,0 +1,10 @@
error: unexpected token at 3..4
NODE_ROOT@0..5
NODE_ATTR_SET@0..2
TOKEN_L_BRACE@0..1 "{"
TOKEN_R_BRACE@1..2 "}"
TOKEN_WHITESPACE@2..3 " "
NODE_ERROR@3..4
TOKEN_ASSIGN@3..4 "="
TOKEN_WHITESPACE@4..5 "\n"
@@ -0,0 +1,2 @@
{} =
@@ -0,0 +1,18 @@
NODE_ROOT@0..9
NODE_BIN_OP@0..9
NODE_APPLY@0..5
NODE_APPLY@0..3
NODE_IDENT@0..1
TOKEN_IDENT@0..1 "a"
TOKEN_WHITESPACE@1..2 " "
NODE_LITERAL@2..3
TOKEN_INTEGER@2..3 "1"
TOKEN_WHITESPACE@3..4 " "
NODE_LITERAL@4..5
TOKEN_INTEGER@4..5 "2"
TOKEN_WHITESPACE@5..6 " "
TOKEN_ADD@6..7 "+"
TOKEN_WHITESPACE@7..8 " "
NODE_LITERAL@8..9
TOKEN_INTEGER@8..9 "3"
@@ -0,0 +1 @@
a 1 2 + 3
@@ -0,0 +1,16 @@
NODE_ROOT@0..18
NODE_ASSERT@0..18
TOKEN_ASSERT@0..6 "assert"
TOKEN_WHITESPACE@6..7 " "
NODE_BIN_OP@7..11
NODE_IDENT@7..8
TOKEN_IDENT@7..8 "a"
TOKEN_EQUAL@8..10 "=="
NODE_IDENT@10..11
TOKEN_IDENT@10..11 "b"
TOKEN_SEMICOLON@11..12 ";"
NODE_STRING@12..18
TOKEN_STRING_START@12..13 "\""
TOKEN_STRING_CONTENT@13..17 "a==b"
TOKEN_STRING_END@17..18 "\""
@@ -0,0 +1 @@
assert a==b;"a==b"
@@ -0,0 +1,19 @@
NODE_ROOT@0..10
NODE_ATTR_SET@0..10
TOKEN_L_BRACE@0..1 "{"
NODE_ATTRPATH_VALUE@1..9
NODE_ATTRPATH@1..6
NODE_IDENT@1..2
TOKEN_IDENT@1..2 "a"
TOKEN_DOT@2..3 "."
NODE_IDENT@3..4
TOKEN_IDENT@3..4 "b"
TOKEN_DOT@4..5 "."
NODE_IDENT@5..6
TOKEN_IDENT@5..6 "c"
TOKEN_ASSIGN@6..7 "="
NODE_LITERAL@7..8
TOKEN_INTEGER@7..8 "1"
TOKEN_SEMICOLON@8..9 ";"
TOKEN_R_BRACE@9..10 "}"
@@ -0,0 +1 @@
{a.b.c=1;}
@@ -0,0 +1,28 @@
NODE_ROOT@0..49
NODE_ATTR_SET@0..49
TOKEN_L_BRACE@0..1 "{"
TOKEN_WHITESPACE@1..4 "\n "
NODE_ATTRPATH_VALUE@4..25
NODE_ATTRPATH@4..19
NODE_IDENT@4..19
TOKEN_IDENT@4..19 "meaning_of_life"
TOKEN_WHITESPACE@19..20 " "
TOKEN_ASSIGN@20..21 "="
TOKEN_WHITESPACE@21..22 " "
NODE_LITERAL@22..24
TOKEN_INTEGER@22..24 "42"
TOKEN_SEMICOLON@24..25 ";"
TOKEN_WHITESPACE@25..28 "\n "
NODE_ATTRPATH_VALUE@28..47
NODE_ATTRPATH@28..38
NODE_IDENT@28..38
TOKEN_IDENT@28..38 "H4X0RNUM83"
TOKEN_WHITESPACE@38..39 " "
TOKEN_ASSIGN@39..40 "="
TOKEN_WHITESPACE@40..41 " "
NODE_LITERAL@41..46
TOKEN_FLOAT@41..46 "1.337"
TOKEN_SEMICOLON@46..47 ";"
TOKEN_WHITESPACE@47..48 "\n"
TOKEN_R_BRACE@48..49 "}"
@@ -0,0 +1,4 @@
{
meaning_of_life = 42;
H4X0RNUM83 = 1.337;
}
@@ -0,0 +1,43 @@
NODE_ROOT@0..33
NODE_ATTR_SET@0..33
TOKEN_L_BRACE@0..1 "{"
TOKEN_WHITESPACE@1..4 "\n "
NODE_ATTRPATH_VALUE@4..12
NODE_ATTRPATH@4..7
NODE_IDENT@4..5
TOKEN_IDENT@4..5 "a"
TOKEN_DOT@5..6 "."
NODE_IDENT@6..7
TOKEN_IDENT@6..7 "b"
TOKEN_WHITESPACE@7..8 " "
TOKEN_ASSIGN@8..9 "="
TOKEN_WHITESPACE@9..10 " "
NODE_LITERAL@10..11
TOKEN_INTEGER@10..11 "2"
TOKEN_SEMICOLON@11..12 ";"
TOKEN_WHITESPACE@12..15 "\n "
NODE_ATTRPATH_VALUE@15..31
NODE_ATTRPATH@15..26
NODE_STRING@15..21
TOKEN_STRING_START@15..16 "\""
NODE_INTERPOL@16..20
TOKEN_INTERPOL_START@16..18 "${"
NODE_IDENT@18..19
TOKEN_IDENT@18..19 "c"
TOKEN_INTERPOL_END@19..20 "}"
TOKEN_STRING_END@20..21 "\""
TOKEN_DOT@21..22 "."
NODE_DYNAMIC@22..26
TOKEN_INTERPOL_START@22..24 "${"
NODE_IDENT@24..25
TOKEN_IDENT@24..25 "d"
TOKEN_INTERPOL_END@25..26 "}"
TOKEN_WHITESPACE@26..27 " "
TOKEN_ASSIGN@27..28 "="
TOKEN_WHITESPACE@28..29 " "
NODE_LITERAL@29..30
TOKEN_INTEGER@29..30 "3"
TOKEN_SEMICOLON@30..31 ";"
TOKEN_WHITESPACE@31..32 "\n"
TOKEN_R_BRACE@32..33 "}"
@@ -0,0 +1,4 @@
{
a.b = 2;
"${c}".${d} = 3;
}
@@ -0,0 +1,5 @@
NODE_ROOT@0..2
NODE_ATTR_SET@0..2
TOKEN_L_BRACE@0..1 "{"
TOKEN_R_BRACE@1..2 "}"
@@ -0,0 +1 @@
{}
@@ -0,0 +1,14 @@
NODE_ROOT@0..12
NODE_ATTR_SET@0..12
TOKEN_REC@0..3 "rec"
TOKEN_L_BRACE@3..4 "{"
NODE_ATTRPATH_VALUE@4..11
NODE_ATTRPATH@4..8
NODE_IDENT@4..8
TOKEN_IDENT@4..8 "test"
TOKEN_ASSIGN@8..9 "="
NODE_LITERAL@9..10
TOKEN_INTEGER@9..10 "1"
TOKEN_SEMICOLON@10..11 ";"
TOKEN_R_BRACE@11..12 "}"
@@ -0,0 +1 @@
rec{test=1;}
@@ -0,0 +1,32 @@
NODE_ROOT@0..20
NODE_BIN_OP@0..20
NODE_BIN_OP@0..3
NODE_LITERAL@0..1
TOKEN_INTEGER@0..1 "1"
TOKEN_LESS@1..2 "<"
NODE_LITERAL@2..3
TOKEN_INTEGER@2..3 "2"
TOKEN_OR_OR@3..5 "||"
NODE_BIN_OP@5..20
NODE_BIN_OP@5..14
NODE_BIN_OP@5..9
NODE_LITERAL@5..6
TOKEN_INTEGER@5..6 "2"
TOKEN_LESS_OR_EQ@6..8 "<="
NODE_LITERAL@8..9
TOKEN_INTEGER@8..9 "2"
TOKEN_AND_AND@9..11 "&&"
NODE_BIN_OP@11..14
NODE_LITERAL@11..12
TOKEN_INTEGER@11..12 "2"
TOKEN_MORE@12..13 ">"
NODE_LITERAL@13..14
TOKEN_INTEGER@13..14 "1"
TOKEN_AND_AND@14..16 "&&"
NODE_BIN_OP@16..20
NODE_LITERAL@16..17
TOKEN_INTEGER@16..17 "2"
TOKEN_MORE_OR_EQ@17..19 ">="
NODE_LITERAL@19..20
TOKEN_INTEGER@19..20 "2"
@@ -0,0 +1 @@
1<2||2<=2&&2>1&&2>=2
@@ -0,0 +1,23 @@
NODE_ROOT@0..33
NODE_BIN_OP@0..33
NODE_IDENT@0..5
TOKEN_IDENT@0..5 "false"
TOKEN_WHITESPACE@5..6 " "
TOKEN_IMPLICATION@6..8 "->"
NODE_BIN_OP@8..33
NODE_BIN_OP@8..27
NODE_UNARY_OP@8..14
TOKEN_INVERT@8..9 "!"
NODE_IDENT@9..14
TOKEN_IDENT@9..14 "false"
TOKEN_AND_AND@14..16 "&&"
NODE_BIN_OP@16..27
NODE_IDENT@16..21
TOKEN_IDENT@16..21 "false"
TOKEN_EQUAL@21..23 "=="
NODE_IDENT@23..27
TOKEN_IDENT@23..27 "true"
TOKEN_OR_OR@27..29 "||"
NODE_IDENT@29..33
TOKEN_IDENT@29..33 "true"
@@ -0,0 +1 @@
false ->!false&&false==true||true
@@ -0,0 +1,16 @@
NODE_ROOT@0..10
NODE_BIN_OP@0..10
NODE_BIN_OP@0..4
NODE_LITERAL@0..1
TOKEN_INTEGER@0..1 "1"
TOKEN_EQUAL@1..3 "=="
NODE_LITERAL@3..4
TOKEN_INTEGER@3..4 "1"
TOKEN_AND_AND@4..6 "&&"
NODE_BIN_OP@6..10
NODE_LITERAL@6..7
TOKEN_INTEGER@6..7 "2"
TOKEN_NOT_EQUAL@7..9 "!="
NODE_LITERAL@9..10
TOKEN_INTEGER@9..10 "3"
@@ -0,0 +1 @@
1==1&&2!=3
Loaded 100 of 492 files, more files were not shown because too many files have changed in this diff. Show more