git.delta.rocks / jrsonnet / refs/commits / 5382799864e9

difftreelog

feat lsp prototype

Yaroslav Bolyukin2022-03-29parent: #609e5ef.patch.diff
in: master

29 files changed

deletedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ /dev/null
@@ -1,984 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "ahash"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
-dependencies = [
- "cfg-if",
- "once_cell",
- "version_check",
-]
-
-[[package]]
-name = "annotate-snippets"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3b9d411ecbaf79885c6df4d75fff75858d5995ff25385657a28af47e82f9c36"
-dependencies = [
- "unicode-width",
- "yansi-term",
-]
-
-[[package]]
-name = "anstream"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
-dependencies = [
- "anstyle",
- "anstyle-parse",
- "anstyle-query",
- "anstyle-wincon",
- "colorchoice",
- "is-terminal",
- "utf8parse",
-]
-
-[[package]]
-name = "anstyle"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
-
-[[package]]
-name = "anstyle-parse"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
-dependencies = [
- "utf8parse",
-]
-
-[[package]]
-name = "anstyle-query"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
-dependencies = [
- "windows-sys",
-]
-
-[[package]]
-name = "anstyle-wincon"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
-dependencies = [
- "anstyle",
- "windows-sys",
-]
-
-[[package]]
-name = "anyhow"
-version = "1.0.72"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
-
-[[package]]
-name = "async-trait"
-version = "0.1.71"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.28",
-]
-
-[[package]]
-name = "autocfg"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
-
-[[package]]
-name = "base64"
-version = "0.21.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
-
-[[package]]
-name = "bincode"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "bitflags"
-version = "1.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
-
-[[package]]
-name = "bitflags"
-version = "2.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
-
-[[package]]
-name = "block-buffer"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "cc"
-version = "1.0.79"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
-
-[[package]]
-name = "cfg-if"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
-
-[[package]]
-name = "clap"
-version = "4.3.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eab9e8ceb9afdade1ab3f0fd8dbce5b1b2f468ad653baf10e771781b2b67b73"
-dependencies = [
- "clap_builder",
- "clap_derive",
- "once_cell",
-]
-
-[[package]]
-name = "clap_builder"
-version = "4.3.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f2763db829349bf00cfc06251268865ed4363b93a943174f638daf3ecdba2cd"
-dependencies = [
- "anstream",
- "anstyle",
- "clap_lex",
- "strsim",
-]
-
-[[package]]
-name = "clap_complete"
-version = "4.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce"
-dependencies = [
- "clap",
-]
-
-[[package]]
-name = "clap_derive"
-version = "4.3.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
-dependencies = [
- "heck",
- "proc-macro2",
- "quote",
- "syn 2.0.28",
-]
-
-[[package]]
-name = "clap_lex"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
-
-[[package]]
-name = "colorchoice"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
-
-[[package]]
-name = "cpufeatures"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
-dependencies = [
- "generic-array",
- "typenum",
-]
-
-[[package]]
-name = "derivative"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "digest"
-version = "0.10.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
-dependencies = [
- "block-buffer",
- "crypto-common",
-]
-
-[[package]]
-name = "errno"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
-dependencies = [
- "errno-dragonfly",
- "libc",
- "windows-sys",
-]
-
-[[package]]
-name = "errno-dragonfly"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
-dependencies = [
- "cc",
- "libc",
-]
-
-[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-
-[[package]]
-name = "hashbrown"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
-dependencies = [
- "ahash",
-]
-
-[[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
-
-[[package]]
-name = "hermit-abi"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
-
-[[package]]
-name = "indexmap"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
-dependencies = [
- "autocfg",
- "hashbrown 0.12.3",
-]
-
-[[package]]
-name = "is-terminal"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
-dependencies = [
- "hermit-abi",
- "rustix",
- "windows-sys",
-]
-
-[[package]]
-name = "itoa"
-version = "1.0.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
-
-[[package]]
-name = "jrsonnet"
-version = "0.5.0-pre95"
-dependencies = [
- "clap",
- "clap_complete",
- "jrsonnet-cli",
- "jrsonnet-evaluator",
- "jrsonnet-gcmodule",
- "jrsonnet-parser",
- "mimallocator",
- "serde",
- "serde_json",
- "thiserror",
-]
-
-[[package]]
-name = "jrsonnet-cli"
-version = "0.5.0-pre95"
-dependencies = [
- "clap",
- "jrsonnet-evaluator",
- "jrsonnet-gcmodule",
- "jrsonnet-parser",
- "jrsonnet-stdlib",
-]
-
-[[package]]
-name = "jrsonnet-evaluator"
-version = "0.5.0-pre95"
-dependencies = [
- "annotate-snippets",
- "anyhow",
- "async-trait",
- "bincode",
- "derivative",
- "hashbrown 0.13.2",
- "jrsonnet-gcmodule",
- "jrsonnet-interner",
- "jrsonnet-macros",
- "jrsonnet-parser",
- "jrsonnet-types",
- "num-bigint",
- "pathdiff",
- "rustc-hash",
- "serde",
- "static_assertions",
- "strsim",
- "thiserror",
-]
-
-[[package]]
-name = "jrsonnet-gcmodule"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c11fb98940a7f8b419619e98ccbf2e094671a5fdd0e277f05acd373071186d57"
-dependencies = [
- "jrsonnet-gcmodule-derive",
- "parking_lot",
-]
-
-[[package]]
-name = "jrsonnet-gcmodule-derive"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bee774b7ba86fc86ee84482cd6732aa860ae3559f9827c65efd75c51e66ac76"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "jrsonnet-interner"
-version = "0.5.0-pre95"
-dependencies = [
- "hashbrown 0.13.2",
- "jrsonnet-gcmodule",
- "rustc-hash",
- "serde",
- "structdump",
-]
-
-[[package]]
-name = "jrsonnet-macros"
-version = "0.5.0-pre95"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "jrsonnet-parser"
-version = "0.5.0-pre95"
-dependencies = [
- "jrsonnet-gcmodule",
- "jrsonnet-interner",
- "peg",
- "serde",
- "static_assertions",
- "structdump",
-]
-
-[[package]]
-name = "jrsonnet-stdlib"
-version = "0.5.0-pre95"
-dependencies = [
- "base64",
- "bincode",
- "jrsonnet-evaluator",
- "jrsonnet-gcmodule",
- "jrsonnet-macros",
- "jrsonnet-parser",
- "md5",
- "num-bigint",
- "serde",
- "serde_json",
- "serde_yaml_with_quirks",
- "sha1",
- "sha2",
- "sha3",
- "structdump",
-]
-
-[[package]]
-name = "jrsonnet-types"
-version = "0.5.0-pre95"
-dependencies = [
- "jrsonnet-gcmodule",
- "peg",
-]
-
-[[package]]
-name = "keccak"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940"
-dependencies = [
- "cpufeatures",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.147"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
-
-[[package]]
-name = "libjsonnet"
-version = "0.5.0-pre95"
-dependencies = [
- "jrsonnet-evaluator",
- "jrsonnet-gcmodule",
- "jrsonnet-parser",
- "jrsonnet-stdlib",
-]
-
-[[package]]
-name = "linked-hash-map"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0"
-
-[[package]]
-name = "lock_api"
-version = "0.4.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
-
-[[package]]
-name = "md5"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
-
-[[package]]
-name = "mimalloc-sys"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4aa3cefb626f6ae3d0b2f71c5378c89d2b1d4d7bc246b0ca9a7ee61a4daad291"
-dependencies = [
- "cc",
- "libc",
-]
-
-[[package]]
-name = "mimallocator"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d44fe4ebf6b538fcf39d9975c2b90bb3232d1ba8e8bffeacd004f27b20c577a"
-dependencies = [
- "mimalloc-sys",
-]
-
-[[package]]
-name = "num-bigint"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
-dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
- "serde",
-]
-
-[[package]]
-name = "num-integer"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
-dependencies = [
- "autocfg",
- "num-traits",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
-
-[[package]]
-name = "parking_lot"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-targets",
-]
-
-[[package]]
-name = "pathdiff"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
-
-[[package]]
-name = "peg"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a07f2cafdc3babeebc087e499118343442b742cc7c31b4d054682cc598508554"
-dependencies = [
- "peg-macros",
- "peg-runtime",
-]
-
-[[package]]
-name = "peg-macros"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a90084dc05cf0428428e3d12399f39faad19b0909f64fb9170c9fdd6d9cd49b"
-dependencies = [
- "peg-runtime",
- "proc-macro2",
- "quote",
-]
-
-[[package]]
-name = "peg-runtime"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.65"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92de25114670a878b1261c79c9f8f729fb97e95bac93f6312f583c60dd6a1dfe"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5907a1b7c277254a8b15170f6e7c97cfa60ee7872a3217663bb81151e48184bb"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
-dependencies = [
- "bitflags 1.3.2",
-]
-
-[[package]]
-name = "rustc-hash"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
-
-[[package]]
-name = "rustix"
-version = "0.38.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
-dependencies = [
- "bitflags 2.3.3",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys",
-]
-
-[[package]]
-name = "ryu"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
-
-[[package]]
-name = "scopeguard"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-
-[[package]]
-name = "serde"
-version = "1.0.171"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.171"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.28",
-]
-
-[[package]]
-name = "serde_json"
-version = "1.0.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
-dependencies = [
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "serde_yaml_with_quirks"
-version = "0.8.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47c5983eba86eae2d0058c35fb1065ccffb23af7f8965871069269088098321a"
-dependencies = [
- "indexmap",
- "ryu",
- "serde",
- "yaml-rust",
-]
-
-[[package]]
-name = "sha1"
-version = "0.10.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "sha2"
-version = "0.10.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "sha3"
-version = "0.10.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
-dependencies = [
- "digest",
- "keccak",
-]
-
-[[package]]
-name = "smallvec"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
-
-[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
-[[package]]
-name = "strsim"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
-
-[[package]]
-name = "structdump"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0570327507bf281d8a6e6b0d4c082b12cb6bcee27efce755aa5efacd44076c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "structdump-derive",
-]
-
-[[package]]
-name = "structdump-derive"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29cc0b59cfa11f1bceda09a9a7e37e6a6c3138575fd24ade8aa9af6d09aedf28"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "tests"
-version = "0.1.0"
-dependencies = [
- "jrsonnet-evaluator",
- "jrsonnet-gcmodule",
- "jrsonnet-stdlib",
- "serde",
-]
-
-[[package]]
-name = "thiserror"
-version = "1.0.43"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.43"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.28",
-]
-
-[[package]]
-name = "typenum"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
-
-[[package]]
-name = "unicode-width"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
-
-[[package]]
-name = "utf8parse"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
-
-[[package]]
-name = "version_check"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
-
-[[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-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
-name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.48.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
-
-[[package]]
-name = "yaml-rust"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
-dependencies = [
- "linked-hash-map",
-]
-
-[[package]]
-name = "yansi-term"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1"
-dependencies = [
- "winapi",
-]
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
 [workspace]
 package.version = "0.5.0-pre95"
 package.repository = "https://github.com/CertainLach/jrsonnet"
-members = ["crates/*", "bindings/jsonnet", "cmds/jrsonnet", "tests"]
+members = ["crates/*", "bindings/jsonnet", "cmds/*", "tests"]
 default-members = ["cmds/jrsonnet"]
 resolver = "2"
 
addedcrates/jrsonnet-rowan-parser/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "jrsonnet-rowan-parser"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.51"
+backtrace = "0.3.63"
+drop_bomb = "0.1.5"
+indoc = "1.0.3"
+logos = "0.12.0"
+miette = { version = "4.2.1", features = ["fancy"] }
+rowan = "0.15.0"
+text-size = "1.1.0"
+thiserror = "1.0.30"
+
+[dev-dependencies]
+backtrace = "0.3.63"
+indoc = "1.0.3"
+insta = "1.10.0"
addedcrates/jrsonnet-rowan-parser/src/binary.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/binary.rs
@@ -0,0 +1,47 @@
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum BinaryOperator {
+	Mul,
+	Div,
+	Mod,
+	Plus,
+	Minus,
+	ShiftLeft,
+	ShiftRight,
+	LessThan,
+	GreaterThan,
+	LessThanOrEqual,
+	GreaterThanOrEqual,
+	Equal,
+	NotEqual,
+	BitAnd,
+	BitXor,
+	BitOr,
+	And,
+	Or,
+	In,
+	ObjectApply,
+	Invalid,
+}
+
+impl BinaryOperator {
+	pub fn binding_power(&self) -> (u8, u8) {
+		match self {
+			Self::ObjectApply => (22, 23),
+			Self::Mul | Self::Div | Self::Mod => (20, 21),
+			Self::Plus | Self::Minus => (18, 19),
+			Self::ShiftLeft | Self::ShiftRight => (16, 17),
+			Self::LessThan
+			| Self::GreaterThan
+			| Self::LessThanOrEqual
+			| Self::GreaterThanOrEqual
+			| Self::In => (14, 15),
+			Self::Equal | Self::NotEqual => (12, 13),
+			Self::BitAnd => (10, 11),
+			Self::BitXor => (8, 9),
+			Self::BitOr => (6, 7),
+			Self::And => (4, 5),
+			Self::Or => (2, 3),
+			Self::Invalid => (0, 1),
+		}
+	}
+}
addedcrates/jrsonnet-rowan-parser/src/event.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/event.rs
@@ -0,0 +1,107 @@
+use std::mem;
+
+use rowan::{GreenNode, GreenNodeBuilder, Language};
+
+use crate::{
+	lex::{Lang, Lexeme, SyntaxKind},
+	parser::{Parse, SyntaxError},
+};
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Event {
+	Start {
+		kind: SyntaxKind,
+		forward_parent: Option<usize>,
+	},
+	Token,
+	Finish,
+	Placeholder,
+	Error(SyntaxError),
+}
+
+pub(super) struct Sink<'i> {
+	pub builder: GreenNodeBuilder<'static>,
+	lexemes: &'i [Lexeme<'i>],
+	offset: usize,
+	events: Vec<Event>,
+	pub errors: Vec<SyntaxError>,
+}
+
+impl<'i> Sink<'i> {
+	pub(super) fn new(events: Vec<Event>, lexemes: &'i [Lexeme<'i>]) -> Self {
+		Self {
+			builder: GreenNodeBuilder::new(),
+			lexemes,
+			offset: 0,
+			events,
+			errors: vec![],
+		}
+	}
+
+	pub(super) fn finish(mut self) -> Parse {
+		for idx in 0..self.events.len() {
+			match mem::replace(&mut self.events[idx], Event::Placeholder) {
+				Event::Start {
+					kind,
+					forward_parent,
+				} => {
+					let mut kinds = vec![kind];
+
+					let mut idx = idx;
+					let mut forward_parent = forward_parent;
+
+					// Walk through the forward parent of the forward parent, and the forward parent
+					// of that, and of that, etc. until we reach a StartNode event without a forward
+					// parent.
+					while let Some(fp) = forward_parent {
+						idx += fp;
+
+						forward_parent = if let Event::Start {
+							kind,
+							forward_parent,
+						} = mem::replace(&mut self.events[idx], Event::Placeholder)
+						{
+							kinds.push(kind);
+							forward_parent
+						} else {
+							unreachable!()
+						};
+					}
+
+					for kind in kinds.into_iter().rev() {
+						self.builder.start_node(Lang::kind_to_raw(kind));
+					}
+				}
+				Event::Token => self.token(),
+				Event::Finish => {
+					self.builder.finish_node();
+				}
+				Event::Placeholder => {}
+				Event::Error(e) => {
+					self.errors.push(e);
+				}
+			}
+			self.skip_whitespace();
+		}
+
+		Parse {
+			green_node: self.builder.finish(),
+			errors: self.errors,
+		}
+	}
+	fn token(&mut self) {
+		let lexeme = self.lexemes[self.offset];
+		self.builder
+			.token(Lang::kind_to_raw(lexeme.kind), lexeme.text);
+		self.offset += 1;
+	}
+	fn skip_whitespace(&mut self) {
+		while let Some(lexeme) = self.lexemes.get(self.offset) {
+			if !lexeme.kind.is_trivia() {
+				break;
+			}
+
+			self.token();
+		}
+	}
+}
addedcrates/jrsonnet-rowan-parser/src/lex.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/lex.rs
@@ -0,0 +1,315 @@
+use crate::string_block::lex_str_block_test;
+use core::ops::Range;
+use logos::Logos;
+use rowan::{Checkpoint, TextRange, TextSize};
+use std::{convert::TryFrom, iter::Peekable};
+
+#[derive(Logos, Debug, PartialEq, Hash, Eq, PartialOrd, Ord, Clone, Copy)]
+#[repr(u16)]
+pub enum SyntaxKind {
+	#[token("assert")]
+	KeywordAssert = 0,
+
+	#[token("else")]
+	KeywordElse,
+
+	#[token("error")]
+	KeywordError,
+
+	#[token("false")]
+	KeywordFalse,
+
+	#[token("for")]
+	KeywordFor,
+
+	#[token("function")]
+	KeywordFunction,
+
+	#[token("if")]
+	KeywordIf,
+
+	#[token("import")]
+	KeywordImport,
+
+	#[token("importstr")]
+	KeywordImportStr,
+
+	#[token("local")]
+	KeywordLocal,
+
+	#[token("null")]
+	KeywordNull,
+
+	#[token("tailstrict")]
+	KeywordTailStrict,
+
+	#[token("then")]
+	KeywordThen,
+
+	#[token("self")]
+	KeywordSelf,
+
+	#[token("super")]
+	KeywordSuper,
+
+	#[token("true")]
+	KeywordTrue,
+
+	#[regex(r"[_a-zA-Z][_a-zA-Z0-9]*")]
+	Ident,
+
+	#[regex(r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?")]
+	Number,
+
+	#[regex(r"(?:0|[1-9][0-9]*)\.[^0-9]")]
+	ErrorNumJunkAfterDecimalPoint,
+
+	#[regex(r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?[eE][^+\-0-9]")]
+	ErrorNumJunkAfterExponent,
+
+	#[regex(r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?[eE][+-][^0-9]")]
+	ErrorNumJunkAfterExponentSign,
+
+	#[token("{")]
+	SymbolLeftBrace,
+
+	#[token("}")]
+	SymbolRightBrace,
+
+	#[token("[")]
+	SymbolLeftBracket,
+
+	#[token("]")]
+	SymbolRightBracket,
+
+	#[token(",")]
+	SymbolComma,
+
+	#[token(".")]
+	SymbolDot,
+
+	#[token("(")]
+	LParen,
+
+	#[token(")")]
+	RParen,
+
+	#[token(";")]
+	SymbolSemi,
+	#[token(":")]
+	SymbolColon,
+
+	#[token("$")]
+	SymbolDollar,
+
+	#[token("*")]
+	OpMul,
+	#[token("/")]
+	OpDiv,
+	#[token("%")]
+	OpMod,
+	#[token("+")]
+	OpPlus,
+	#[token("-")]
+	OpMinus,
+	#[token("<<")]
+	OpShiftLeft,
+	#[token(">>")]
+	OpShiftRight,
+	#[token("<")]
+	OpLessThan,
+	#[token(">")]
+	OpGreaterThan,
+	#[token("<=")]
+	OpLessThanOrEqual,
+	#[token(">=")]
+	OpGreaterThanOrEqual,
+	#[token("==")]
+	OpEqual,
+	#[token("!=")]
+	OpNotEqual,
+	#[token("&")]
+	OpBitAnd,
+	#[token("^")]
+	OpBitXor,
+	#[token("|")]
+	OpBitOr,
+	#[token("&&")]
+	OpAnd,
+	#[token("||")]
+	OpOr,
+	#[token("in")]
+	OpIn,
+	#[token("!")]
+	OpNot,
+	#[token("~")]
+	OpBitNegate,
+	#[token("=")]
+	SymbolAssign,
+
+	#[regex("\"(?s:[^\"\\\\]|\\\\.)*\"")]
+	StringDoubleQuoted,
+
+	#[regex("'(?s:[^'\\\\]|\\\\.)*'")]
+	StringSingleQuoted,
+
+	#[regex("@\"(?:[^\"]|\"\")*\"")]
+	StringDoubleVerbatim,
+
+	#[regex("@'(?:[^']|'')*'")]
+	StringSingleVerbatim,
+
+	#[regex(r"\|\|\|", lex_str_block_test)]
+	StringBlock, //(StringBlockToken),
+
+	#[regex("\"(?s:[^\"\\\\]|\\\\.)*")]
+	ErrorStringDoubleQuotedUnterminated,
+
+	#[regex("'(?s:[^'\\\\]|\\\\.)*")]
+	ErrorStringSingleQuotedUnterminated,
+
+	#[regex("@\"(?:[^\"]|\"\")*")]
+	ErrorStringDoubleVerbatimUnterminated,
+
+	#[regex("@'(?:[^']|'')*")]
+	ErrorStringSingleVerbatimUnterminated,
+
+	#[regex("@[^\"'\\s]\\S+")]
+	ErrorStringMissingQuotes,
+
+	#[token("/*/")]
+	ErrorCommentTooShort,
+
+	#[regex(r"/\*([^*]|\*[^/])+")]
+	ErrorCommentUnterminated,
+
+	#[regex(r"[ \t\n\r]+")]
+	Whitespace,
+
+	#[regex(r"//[^\r\n]*(\r\n|\n)?")]
+	SingelLineSlashComment,
+
+	#[regex(r"#[^\r\n]*(\r\n|\n)?")]
+	SingleLineHashComment,
+
+	#[regex(r"/\*([^*]|\*[^/])*\*/")]
+	MultiLineComment,
+
+	#[error]
+	Error,
+
+	ErrorPositionalAfterNamed,
+
+	Literal,
+	Expr,
+	Array,
+	ArrayElem,
+	Object,
+	Field,
+
+	CompspecFor,
+	CompspecIf,
+
+	Slice,
+	FieldAccess,
+	ObjectApply,
+	FunctionCall,
+	FunctionDef,
+	BodyDef,
+
+	BinOp,
+	UnaryOp,
+	Local,
+	ExprError,
+	ExprAssert,
+	ExprImport,
+
+	DefParam,
+	DefParams,
+
+	DefArgs,
+	DefNamedArg,
+	DefPositionalArg,
+
+	Parened,
+
+	Root,
+}
+
+impl SyntaxKind {
+	pub fn is_trivia(self) -> bool {
+		matches!(
+			self,
+			Self::Whitespace
+				| Self::MultiLineComment
+				| Self::SingelLineSlashComment
+				| Self::SingleLineHashComment
+		)
+	}
+}
+
+pub struct Lexer<'a> {
+	inner: logos::Lexer<'a, SyntaxKind>,
+}
+
+impl<'a> Lexer<'a> {
+	pub fn new(input: &'a str) -> Self {
+		Self {
+			inner: SyntaxKind::lexer(input),
+		}
+	}
+}
+
+impl<'a> Iterator for Lexer<'a> {
+	type Item = Lexeme<'a>;
+
+	fn next(&mut self) -> Option<Self::Item> {
+		let kind = self.inner.next()?;
+		let text = self.inner.slice();
+
+		Some(Self::Item {
+			kind,
+			text,
+			range: {
+				let Range { start, end } = self.inner.span();
+
+				TextRange::new(
+					TextSize::try_from(start).unwrap(),
+					TextSize::try_from(end).unwrap(),
+				)
+			},
+		})
+	}
+}
+
+#[derive(Clone, Copy)]
+pub struct Lexeme<'i> {
+	pub kind: SyntaxKind,
+	pub text: &'i str,
+	pub range: TextRange,
+}
+
+pub fn lex(input: &str) -> Vec<Lexeme<'_>> {
+	Lexer::new(input).collect()
+}
+
+impl From<SyntaxKind> for rowan::SyntaxKind {
+	fn from(kind: SyntaxKind) -> Self {
+		Self(kind as u16)
+	}
+}
+
+use SyntaxKind::*;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Lang {}
+impl rowan::Language for Lang {
+	type Kind = SyntaxKind;
+	fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
+		assert!(raw.0 <= Root as u16);
+		unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
+	}
+	fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
+		kind.into()
+	}
+}
addedcrates/jrsonnet-rowan-parser/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/lib.rs
@@ -0,0 +1,139 @@
+#![deny(unused_must_use)]
+
+mod binary;
+mod event;
+mod lex;
+mod marker;
+mod parser;
+mod string_block;
+mod token_set;
+mod unary;
+
+#[cfg(test)]
+mod tests {
+	use miette::{Diagnostic, GraphicalReportHandler, LabeledSpan};
+	use thiserror::Error;
+
+	use crate::parser::parse;
+
+	#[derive(Debug, Error)]
+	#[error("syntax error")]
+	struct MyDiagnostic {
+		code: String,
+		spans: Vec<LabeledSpan>,
+	}
+	impl Diagnostic for MyDiagnostic {
+		fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+			None
+		}
+
+		fn severity(&self) -> Option<miette::Severity> {
+			None
+		}
+
+		fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+			None
+		}
+
+		fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+			None
+		}
+
+		fn source_code(&self) -> Option<&dyn miette::SourceCode> {
+			Some(&self.code)
+		}
+
+		fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
+			Some(Box::new(self.spans.clone().into_iter()))
+		}
+
+		fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
+			None
+		}
+	}
+
+	fn process(text: &str) -> String {
+		use std::fmt::Write;
+		let mut out = String::new();
+		let node = parse(text);
+		write!(out, "{:#?}", node.syntax()).unwrap();
+		if !node.errors.is_empty() && !text.is_empty() {
+			writeln!(out, "===").unwrap();
+			for err in &node.errors {
+				writeln!(out, "{:?}", err).unwrap();
+			}
+			let diag = MyDiagnostic {
+				code: text.to_string(),
+				spans: node.errors.into_iter().map(|e| e.into()).collect(),
+			};
+
+			let handler = GraphicalReportHandler::new();
+
+			write!(out, "===").unwrap();
+			handler.render_report(&mut out, &diag).unwrap();
+		}
+		out
+	}
+	macro_rules! mk_test {
+		($($name:ident => $test:expr)+) => {$(
+			#[test]
+			fn $name() {
+				let src = indoc::indoc!($test);
+				let result = process(&src);
+				insta::assert_snapshot!(stringify!($name), result, src);
+
+			}
+		)+};
+	}
+	mk_test!(
+		empty => r#" "#
+		function => r#"
+			function(a, b = 1) a + b
+		"#
+		function_error_no_value => r#"
+			function(a, b = ) a + b
+		"#
+		function_error_rparen => r#"
+			function(a, b
+		"#
+		function_error_body => r#"
+			function(a, b)
+		"#
+		local_novalue => r#"
+			local a =
+		"#
+		local_no_value_recovery => r#"
+			local a =
+			local b = 3;
+			1
+		"#
+
+		array_comp => r#"
+			[a for a in [1, 2, 3]]
+		"#
+		array_comp_incompatible_with_multiple_elems => r#"
+			[a for a in [1, 2, 3], b]
+		"#
+
+		no_rhs => r#"
+			a +
+		"#
+		no_lhs => r#"
+			+ 2
+		"#
+		no_operator => "
+			2 2
+		"
+
+		named_before_positional => "
+			a(1, 2, b=4, 3, 5, k = 12, 6)
+		"
+
+		wrong_field_end => "
+			{
+				a: 1;
+				b: 2;
+			}
+		"
+	);
+}
addedcrates/jrsonnet-rowan-parser/src/marker.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/marker.rs
@@ -0,0 +1,114 @@
+use drop_bomb::DropBomb;
+use rowan::TextRange;
+
+use crate::{event::Event, lex::SyntaxKind, parser::Parser};
+
+pub struct Ranger {
+	pub pos: usize,
+}
+impl Ranger {
+	pub fn finish(mut self, p: &Parser) -> FinishedRanger {
+		FinishedRanger {
+			start_token: self.pos,
+			end_token: self.pos.max(p.offset.saturating_sub(1)),
+		}
+	}
+}
+
+pub struct FinishedRanger {
+	pub start_token: usize,
+	pub end_token: usize,
+}
+impl FinishedRanger {
+	pub fn had_error_since(&self, p: &Parser) -> bool {
+		p.last_error_token >= self.start_token
+	}
+}
+
+#[must_use]
+pub struct Marker {
+	pub start_event_idx: usize,
+	pub token: usize,
+	bomb: DropBomb,
+}
+impl Marker {
+	pub fn new(pos: usize, token: usize) -> Self {
+		Self {
+			start_event_idx: pos,
+			token,
+			bomb: DropBomb::new("marked dropped while not completed"),
+		}
+	}
+	pub fn complete(mut self, p: &mut Parser, kind: SyntaxKind) -> CompletedMarker {
+		self.bomb.defuse();
+		let event_at_pos = &mut p.events[self.start_event_idx];
+		assert_eq!(*event_at_pos, Event::Placeholder);
+
+		*event_at_pos = Event::Start {
+			kind,
+			forward_parent: None,
+		};
+
+		p.events.push(Event::Finish);
+		p.entered -= 1;
+		p.clear_outdated_hints();
+		CompletedMarker {
+			start_event_idx: self.start_event_idx,
+			start_token: self.token,
+			end_token: self.token.max(p.offset.saturating_sub(1)),
+		}
+	}
+}
+pub struct CompletedMarker {
+	start_event_idx: usize,
+	pub start_token: usize,
+	pub end_token: usize,
+}
+impl CompletedMarker {
+	pub(super) fn precede(self, p: &mut Parser) -> Marker {
+		let mut new_m = p.start();
+		new_m.token = self.start_token;
+
+		if let Event::Start {
+			ref mut forward_parent,
+			..
+		} = p.events[self.start_event_idx]
+		{
+			*forward_parent = Some(new_m.start_event_idx - self.start_event_idx);
+		} else {
+			unreachable!();
+		}
+
+		new_m
+	}
+}
+
+pub trait AsRange {
+	fn as_range(&self, p: &Parser) -> TextRange;
+	fn end_token(&self) -> usize;
+}
+
+impl AsRange for CompletedMarker {
+	fn as_range(&self, p: &Parser) -> TextRange {
+		TextRange::new(
+			p.start_of_token(self.start_token),
+			p.end_of_token(self.end_token),
+		)
+	}
+	fn end_token(&self) -> usize {
+		self.end_token
+	}
+}
+
+impl AsRange for FinishedRanger {
+	fn as_range(&self, p: &Parser) -> TextRange {
+		TextRange::new(
+			p.start_of_token(self.start_token),
+			p.end_of_token(self.end_token),
+		)
+	}
+
+	fn end_token(&self) -> usize {
+		self.end_token
+	}
+}
addedcrates/jrsonnet-rowan-parser/src/parser.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/parser.rs
@@ -0,0 +1,809 @@
+use std::cell::Cell;
+use std::fmt::Display;
+use std::rc::Rc;
+
+use miette::Diagnostic;
+use miette::LabeledSpan;
+use miette::SourceOffset;
+use miette::SourceSpan;
+use rowan::GreenNode;
+
+use rowan::TextRange;
+use rowan::TextSize;
+use thiserror::Error;
+
+use crate::binary::BinaryOperator;
+use crate::event::Event;
+use crate::event::Sink;
+use crate::lex::lex;
+use crate::lex::Lang;
+use crate::lex::Lexeme;
+use crate::lex::SyntaxKind;
+use crate::lex::SyntaxKind::*;
+use crate::marker::AsRange;
+use crate::marker::CompletedMarker;
+use crate::marker::FinishedRanger;
+use crate::marker::Marker;
+use crate::marker::Ranger;
+use crate::token_set::TokenSet;
+use crate::unary::UnaryOperator;
+
+pub struct Parse {
+	pub green_node: GreenNode,
+	pub errors: Vec<SyntaxError>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ExpectedSyntax {
+	Named(&'static str),
+	Unnamed(SyntaxKind),
+}
+impl Display for ExpectedSyntax {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		match self {
+			ExpectedSyntax::Named(n) => write!(f, "{}", n),
+			ExpectedSyntax::Unnamed(u) => write!(f, "{:?}", u),
+		}
+	}
+}
+
+pub struct Parser<'i> {
+	lexemes: &'i [Lexeme<'i>],
+	pub offset: usize,
+	pub events: Vec<Event>,
+	pub entered: u32,
+	pub hints: Vec<(u32, TextRange, String)>,
+	pub last_error_token: usize,
+	expected_syntax: Option<ExpectedSyntax>,
+	expected_syntax_tracking_state: Rc<Cell<ExpectedSyntaxTrackingState>>,
+}
+
+const DEFAULT_RECOVERY_SET: TokenSet = TokenSet::new(&[
+	SymbolSemi,
+	RParen,
+	SymbolRightBracket,
+	SymbolRightBrace,
+	KeywordLocal,
+]);
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum SyntaxError {
+	Unexpected {
+		expected: ExpectedSyntax,
+		found: SyntaxKind,
+		range: TextRange,
+	},
+	Missing {
+		expected: ExpectedSyntax,
+		offset: TextSize,
+	},
+	Custom {
+		error: String,
+		range: TextRange,
+	},
+	Hint {
+		error: String,
+		range: TextRange,
+	},
+}
+
+impl Into<LabeledSpan> for SyntaxError {
+	fn into(self) -> LabeledSpan {
+		match self {
+			SyntaxError::Unexpected {
+				expected,
+				found,
+				range,
+			} => LabeledSpan::new_with_span(
+				Some(format!("expected {}, found {:?}", expected, found)),
+				SourceSpan::new(
+					SourceOffset::from(usize::from(range.start())),
+					SourceOffset::from(usize::from(range.end() - range.start())),
+				),
+			),
+			SyntaxError::Missing { expected, offset } => LabeledSpan::new_with_span(
+				Some(format!("missing {}", expected)),
+				SourceSpan::new(
+					SourceOffset::from(usize::from(offset)),
+					SourceOffset::from(0),
+				),
+			),
+			SyntaxError::Custom { error, range } | SyntaxError::Hint { error, range } => {
+				LabeledSpan::new_with_span(
+					Some(format!("{}", error)),
+					SourceSpan::new(
+						SourceOffset::from(usize::from(range.start())),
+						SourceOffset::from(usize::from(range.end() - range.start())),
+					),
+				)
+			}
+		}
+	}
+}
+
+impl<'i> Parser<'i> {
+	fn new(lexemes: &'i [Lexeme<'i>]) -> Self {
+		Self {
+			lexemes,
+			offset: 0,
+			events: vec![],
+			entered: 0,
+			last_error_token: 0,
+			hints: vec![],
+			expected_syntax: None,
+			expected_syntax_tracking_state: Rc::new(Cell::new(
+				ExpectedSyntaxTrackingState::Unnamed,
+			)),
+		}
+	}
+	pub fn clear_outdated_hints(&mut self) {
+		let amount = self
+			.hints
+			.iter()
+			.rev()
+			.take_while(|h| h.0 > self.entered)
+			.count();
+		self.hints.truncate(self.hints.len() - amount)
+	}
+	fn clear_expected_syntaxes(&mut self) {
+		self.expected_syntax = None;
+		self.expected_syntax_tracking_state
+			.set(ExpectedSyntaxTrackingState::Unnamed);
+	}
+	pub fn start(&mut self) -> Marker {
+		let start_event_idx = self.events.len();
+		self.events.push(Event::Placeholder);
+		self.entered += 1;
+		Marker::new(start_event_idx, self.offset)
+	}
+	pub fn start_ranger(&mut self) -> Ranger {
+		let pos = self.offset;
+		Ranger { pos }
+	}
+	fn parse(mut self) -> Vec<Event> {
+		let m = self.start();
+		expr(&mut self);
+		if !self.at_end() {
+			let ranger = self.start_ranger();
+
+			while self.peek().is_some() {
+				self.bump()
+			}
+			let end = ranger.finish(&self);
+			self.custom_error(end, "unexpected input after expression");
+		}
+		m.complete(&mut self, Root);
+
+		self.events
+	}
+
+	pub(crate) fn expect(&mut self, kind: SyntaxKind) {
+		self.expect_with_recovery_set(kind, TokenSet::default())
+	}
+
+	pub(crate) fn expect_with_recovery_set(&mut self, kind: SyntaxKind, recovery_set: TokenSet) {
+		if self.at(kind) {
+			self.bump();
+		} else {
+			self.error_with_recovery_set(recovery_set);
+		}
+	}
+
+	pub(crate) fn expect_with_no_skip(&mut self, kind: SyntaxKind) {
+		if self.at(kind) {
+			self.bump();
+		} else {
+			self.error_with_no_skip();
+		}
+	}
+	pub(crate) fn last_token_range(&self) -> Option<TextRange> {
+		self.lexemes.last().map(|Lexeme { range, .. }| *range)
+	}
+	fn current_token(&self) -> Lexeme<'i> {
+		self.lexemes[self.offset]
+	}
+	fn previous_token(&mut self) -> Option<Lexeme<'i>> {
+		if self.offset == 0 {
+			return None;
+		}
+		let mut previous_token_idx = self.offset - 1;
+		while self
+			.lexemes
+			.get(previous_token_idx)
+			.map_or(false, |l| l.kind.is_trivia())
+			&& previous_token_idx != 0
+		{
+			previous_token_idx -= 1;
+		}
+
+		Some(self.lexemes[previous_token_idx])
+	}
+	pub fn start_of_token(&self, mut idx: usize) -> TextSize {
+		while self.lexemes[idx].kind.is_trivia() {
+			idx += 1;
+		}
+		self.lexemes[idx].range.start()
+	}
+	pub fn end_of_token(&self, mut idx: usize) -> TextSize {
+		while self.lexemes[idx].kind.is_trivia() {
+			idx -= 1;
+		}
+		self.lexemes[idx].range.end()
+	}
+	pub(crate) fn custom_error(&mut self, marker: impl AsRange, error: impl AsRef<str>) {
+		self.last_error_token = marker.end_token();
+		self.events.push(Event::Error(SyntaxError::Custom {
+			error: error.as_ref().to_string(),
+			range: marker.as_range(self),
+		}));
+	}
+	pub(crate) fn error_with_recovery_set(
+		&mut self,
+		recovery_set: TokenSet,
+	) -> Option<CompletedMarker> {
+		self.error_with_recovery_set_no_default(recovery_set.union(DEFAULT_RECOVERY_SET))
+	}
+	pub fn error_with_no_skip(&mut self) -> Option<CompletedMarker> {
+		self.error_with_recovery_set_no_default(TokenSet::ALL)
+	}
+
+	pub fn error_with_recovery_set_no_default(
+		&mut self,
+		recovery_set: TokenSet,
+	) -> Option<CompletedMarker> {
+		let expected_syntax = self.expected_syntax.take().unwrap();
+		self.expected_syntax_tracking_state
+			.set(ExpectedSyntaxTrackingState::Unnamed);
+
+		if self.at_end() || self.at_set(recovery_set) {
+			let range = self
+				.previous_token()
+				.map(|t| t.range)
+				.unwrap_or(TextRange::at(TextSize::from(0), TextSize::from(0)));
+
+			self.events.push(Event::Error(SyntaxError::Missing {
+				expected: expected_syntax,
+				offset: range.end(),
+			}));
+			return None;
+		}
+
+		let current_token = self.current_token();
+
+		self.events.push(Event::Error(SyntaxError::Unexpected {
+			expected: expected_syntax.clone(),
+			found: current_token.kind,
+			range: current_token.range,
+		}));
+		self.clear_expected_syntaxes();
+		self.last_error_token = self.offset;
+
+		let m = self.start();
+		self.bump();
+		Some(m.complete(self, SyntaxKind::Error))
+	}
+
+	fn bump(&mut self) {
+		self.skip_trivia();
+		assert_ne!(self.offset, self.lexemes.len(), "already at end");
+		self.events.push(Event::Token);
+		self.offset += 1;
+		self.clear_expected_syntaxes();
+	}
+	fn peek(&mut self) -> Option<SyntaxKind> {
+		self.skip_trivia();
+		self.peek_raw()
+	}
+	pub fn peek_token(&mut self) -> Option<&Lexeme<'i>> {
+		self.skip_trivia();
+		self.peek_token_raw()
+	}
+	fn skip_trivia(&mut self) {
+		while self.peek_raw().map(|c| c.is_trivia()).unwrap_or(false) {
+			self.offset += 1;
+		}
+	}
+	fn peek_raw(&mut self) -> Option<SyntaxKind> {
+		self.lexemes.get(self.offset).map(|l| l.kind)
+	}
+	fn peek_token_raw(&mut self) -> Option<&Lexeme<'i>> {
+		self.lexemes.get(self.offset)
+	}
+	#[must_use]
+	pub(crate) fn expected_syntax_name(&mut self, name: &'static str) -> ExpectedSyntaxGuard {
+		self.expected_syntax_tracking_state
+			.set(ExpectedSyntaxTrackingState::Named);
+		self.expected_syntax = Some(ExpectedSyntax::Named(name));
+
+		ExpectedSyntaxGuard::new(Rc::clone(&self.expected_syntax_tracking_state))
+	}
+	pub fn at(&mut self, kind: SyntaxKind) -> bool {
+		if let ExpectedSyntaxTrackingState::Unnamed = self.expected_syntax_tracking_state.get() {
+			self.expected_syntax = Some(ExpectedSyntax::Unnamed(kind));
+		}
+		self.peek() == Some(kind)
+	}
+	pub fn at_set(&mut self, set: TokenSet) -> bool {
+		self.peek().map_or(false, |k| set.contains(k))
+	}
+	pub fn at_end(&mut self) -> bool {
+		self.peek().is_none()
+	}
+}
+pub(crate) struct ExpectedSyntaxGuard {
+	expected_syntax_tracking_state: Rc<Cell<ExpectedSyntaxTrackingState>>,
+}
+
+impl ExpectedSyntaxGuard {
+	fn new(expected_syntax_tracking_state: Rc<Cell<ExpectedSyntaxTrackingState>>) -> Self {
+		Self {
+			expected_syntax_tracking_state,
+		}
+	}
+}
+
+impl Drop for ExpectedSyntaxGuard {
+	fn drop(&mut self) {
+		self.expected_syntax_tracking_state
+			.set(ExpectedSyntaxTrackingState::Unnamed);
+	}
+}
+
+#[derive(Debug, Clone, Copy)]
+enum ExpectedSyntaxTrackingState {
+	Named,
+	Unnamed,
+}
+macro_rules! at_match {
+	($p:ident {
+		$($r:ident => $e:expr,)*
+		_ => $else:expr $(,)?
+	}) => {{
+		$(
+			if $p.at($r) {$e} else
+		)* {
+			$else
+		}
+	}}
+}
+
+fn expr(p: &mut Parser) {
+	expr_binding_power(p, 0);
+}
+fn expr_binding_power(p: &mut Parser, minimum_binding_power: u8) -> Option<CompletedMarker> {
+	let mut lhs = lhs(p)?;
+
+	loop {
+		let op = at_match!(p {
+			OpMul => BinaryOperator::Mul,
+			OpDiv => BinaryOperator::Div,
+			OpMod => BinaryOperator::Mod,
+			OpPlus => BinaryOperator::Plus,
+			OpMinus => BinaryOperator::Minus,
+			OpShiftLeft => BinaryOperator::ShiftLeft,
+			OpShiftRight => BinaryOperator::ShiftRight,
+			OpLessThan => BinaryOperator::LessThan,
+			OpGreaterThan => BinaryOperator::GreaterThan,
+			OpLessThanOrEqual => BinaryOperator::LessThanOrEqual,
+			OpGreaterThanOrEqual => BinaryOperator::GreaterThanOrEqual,
+			OpEqual => BinaryOperator::Equal,
+			OpNotEqual => BinaryOperator::NotEqual,
+			OpBitAnd => BinaryOperator::BitAnd,
+			OpBitXor => BinaryOperator::BitXor,
+			OpBitOr => BinaryOperator::BitOr,
+			OpAnd => BinaryOperator::And,
+			OpOr => BinaryOperator::Or,
+			OpIn => BinaryOperator::In,
+			SymbolLeftBrace => BinaryOperator::ObjectApply,
+			_ => break,
+		});
+		let (left_binding_power, right_binding_power) = op.binding_power();
+		if left_binding_power < minimum_binding_power {
+			break;
+		}
+
+		// Object apply is not a real operator, we dont have something to bump
+		if op != BinaryOperator::ObjectApply {
+			p.bump();
+		}
+
+		let m = lhs.precede(p);
+		let parsed_rhs = expr_binding_power(p, right_binding_power).is_some();
+		lhs = m.complete(
+			p,
+			if op == BinaryOperator::ObjectApply {
+				ObjectApply
+			} else {
+				BinOp
+			},
+		);
+
+		if !parsed_rhs {
+			break;
+		}
+	}
+	Some(lhs)
+}
+fn compspec(p: &mut Parser) {
+	assert!(p.at(KeywordFor) || p.at(KeywordIf));
+	if p.at(KeywordFor) {
+		let m = p.start();
+		p.bump();
+		p.expect(Ident);
+		p.expect(OpIn);
+		expr(p);
+		m.complete(p, CompspecFor);
+	} else if p.at(KeywordIf) {
+		let m = p.start();
+		p.bump();
+		expr(p);
+		m.complete(p, CompspecIf);
+	} else {
+		unreachable!()
+	}
+}
+fn comma(p: &mut Parser) -> bool {
+	if p.at(SymbolComma) {
+		p.bump();
+		true
+	} else {
+		false
+	}
+}
+fn comma_with_alternatives(p: &mut Parser, set: TokenSet) -> bool {
+	if p.at(SymbolComma) {
+		p.bump();
+		true
+	} else if p.at_set(set) {
+		p.expect_with_no_skip(SymbolComma);
+		p.bump();
+		true
+	} else {
+		false
+	}
+}
+fn field_name(p: &mut Parser) {
+	let _e = p.expected_syntax_name("field name");
+	if p.at(SymbolLeftBracket) {
+		p.bump();
+		expr(p);
+		p.expect(SymbolRightBracket);
+	} else if p.at(Ident) {
+		p.bump()
+	} else {
+		p.error_with_recovery_set(TokenSet::new(&[SymbolSemi]));
+	}
+}
+fn object(p: &mut Parser) -> CompletedMarker {
+	assert!(p.at(SymbolLeftBrace));
+	let m = p.start();
+	p.bump();
+
+	loop {
+		if p.at(SymbolRightBrace) {
+			p.bump();
+			break;
+		}
+		let m = p.start();
+		field_name(p);
+		p.expect(SymbolColon);
+		expr(p);
+		while p.at(KeywordFor) || p.at(KeywordIf) {
+			compspec(p)
+		}
+		m.complete(p, Field);
+		if comma_with_alternatives(p, TokenSet::new(&[SymbolAssign])) {
+			continue;
+		}
+		p.expect(SymbolRightBrace);
+		break;
+	}
+
+	m.complete(p, Object)
+}
+
+fn params(p: &mut Parser) -> CompletedMarker {
+	assert!(p.at(LParen));
+	let m = p.start();
+	p.bump();
+
+	loop {
+		if p.at(RParen) {
+			p.bump();
+			break;
+		}
+		let m = p.start();
+		p.expect(Ident);
+		if p.at(SymbolAssign) {
+			p.bump();
+			expr(p);
+		}
+		m.complete(p, DefParam);
+		if comma(p) {
+			continue;
+		}
+		p.expect(RParen);
+		break;
+	}
+
+	m.complete(p, DefParams)
+}
+fn args(p: &mut Parser) {
+	assert!(p.at(LParen));
+	p.bump();
+
+	let mut error_positional_start = None::<Marker>;
+	let mut started_named = Cell::new(false);
+	let mut on_positional = |p: &mut Parser, m: Marker| {
+		let c = m.complete(p, DefPositionalArg);
+		if started_named.get() && error_positional_start.is_none() {
+			error_positional_start = Some(c.precede(p));
+		}
+	};
+	loop {
+		if p.at(RParen) {
+			break;
+		}
+
+		let m = p.start();
+		if p.at(Ident) {
+			p.bump();
+			if p.at(SymbolAssign) {
+				p.bump();
+				expr(p);
+				m.complete(p, DefNamedArg);
+				started_named.set(true);
+			} else {
+				on_positional(p, m);
+			}
+		} else {
+			expr(p);
+			on_positional(p, m);
+		}
+		if comma(p) {
+			continue;
+		}
+		break;
+	}
+	if let Some(error_positional_start) = error_positional_start {
+		let c = error_positional_start.complete(p, ErrorPositionalAfterNamed);
+		p.custom_error(c, "positional arguments can't be placed after named")
+	}
+	p.expect(RParen);
+}
+
+fn array(p: &mut Parser) -> CompletedMarker {
+	assert!(p.at(SymbolLeftBracket));
+	// Start the list node
+	let m = p.start();
+	p.bump(); // '['
+
+	// This vec will have at most one element in case of correct input
+	let mut compspecs = Vec::with_capacity(1);
+	let mut elems = 0;
+
+	loop {
+		if p.at(SymbolRightBracket) {
+			p.bump();
+			break;
+		}
+		elems += 1;
+		let m = p.start();
+		{
+			let m = p.start();
+			expr(p);
+			m.complete(p, BodyDef);
+		}
+		let c = p.start_ranger();
+		let mut had_spec = false;
+		while p.at(KeywordFor) || p.at(KeywordIf) {
+			had_spec = true;
+			compspec(p)
+		}
+		if had_spec {
+			compspecs.push(c.finish(p));
+		}
+		m.complete(p, ArrayElem);
+		if comma(p) {
+			continue;
+		}
+		p.expect(SymbolRightBracket);
+		break;
+	}
+
+	if elems > 1 && !compspecs.is_empty() {
+		for spec in compspecs {
+			p.custom_error(
+				spec,
+				"compspec may only be used if there is only one array element",
+			)
+		}
+	}
+
+	m.complete(p, Array)
+}
+
+fn lhs(p: &mut Parser) -> Option<CompletedMarker> {
+	let mut lhs = lhs_basic(p)?;
+
+	loop {
+		if p.at(SymbolDot) {
+			let m = lhs.precede(p);
+			p.bump();
+			p.expect(Ident);
+			lhs = m.complete(p, FieldAccess);
+		} else if p.at(SymbolLeftBracket) {
+			let m = lhs.precede(p);
+			p.bump();
+			// Start
+			if !p.at(SymbolColon) {
+				expr(p);
+			}
+			if p.at(SymbolColon) {
+				p.bump();
+				// End
+				if !p.at(SymbolRightBracket) && !p.at(SymbolColon) {
+					expr(p);
+				}
+				if p.at(SymbolColon) {
+					p.bump();
+					// Step
+					if !p.at(SymbolRightBracket) {
+						expr(p);
+					}
+				}
+			}
+			p.expect(SymbolRightBracket);
+			lhs = m.complete(p, Slice);
+		} else if p.at(LParen) {
+			let m = lhs.precede(p);
+			args(p);
+			lhs = m.complete(p, FunctionCall);
+		} else {
+			break;
+		}
+	}
+
+	Some(lhs)
+}
+
+fn lhs_basic(p: &mut Parser) -> Option<CompletedMarker> {
+	let _e = p.expected_syntax_name("value");
+	Some(
+		if p.at(Number)
+			|| p.at(StringSingleQuoted)
+			|| p.at(StringDoubleQuoted)
+			|| p.at(StringSingleVerbatim)
+			|| p.at(StringDoubleVerbatim)
+			|| p.at(StringBlock)
+			|| p.at(KeywordNull)
+			|| p.at(SymbolDollar)
+			|| p.at(KeywordSuper)
+			|| p.at(KeywordSelf)
+		{
+			let m = p.start();
+			p.bump();
+			m.complete(p, Literal)
+		} else if p.at(Ident) {
+			let m = p.start();
+			p.bump();
+			m.complete(p, Ident)
+		} else if p.at(SymbolLeftBracket) {
+			array(p)
+		} else if p.at(SymbolLeftBrace) {
+			object(p)
+		} else if p.at(KeywordLocal) {
+			let m = p.start();
+			p.bump();
+			let mut sus_local = None;
+			loop {
+				p.expect_with_recovery_set(
+					Ident,
+					TokenSet::new(&[SymbolAssign, SymbolSemi, KeywordLocal]),
+				);
+				if p.at(LParen) {
+					params(p);
+				}
+
+				let sus_local_candidate = p.start_ranger();
+				p.expect_with_recovery_set(
+					SymbolAssign,
+					TokenSet::new(&[SymbolSemi, KeywordLocal]),
+				);
+
+				sus_local = p.at(KeywordLocal).then(|| sus_local_candidate.finish(p));
+				expr(p);
+
+				if !comma(p) {
+					break;
+				}
+			}
+			p.expect(SymbolSemi);
+			if let Some(sus_local) = sus_local {
+				if sus_local.had_error_since(p) {
+					p.custom_error(sus_local, "unusal local placement, missing ';' ?")
+				}
+			}
+			{
+				let m = p.start();
+				expr(p);
+				m.complete(p, BodyDef);
+			}
+			m.complete(p, Local)
+		} else if p.at(KeywordFunction) {
+			let m = p.start();
+			p.bump();
+			args(p);
+			{
+				let m = p.start();
+				expr(p);
+				m.complete(p, BodyDef);
+			}
+			m.complete(p, FunctionDef)
+		} else if p.at(KeywordError) {
+			let m = p.start();
+			p.bump();
+			expr(p);
+			m.complete(p, ExprError)
+		} else if p.at(KeywordAssert) {
+			let m = p.start();
+			p.bump();
+			expr(p);
+			if p.at(SymbolColon) {
+				p.bump();
+				expr(p);
+			}
+			m.complete(p, ExprAssert)
+		} else if p.at(KeywordImport) || p.at(KeywordImportStr) {
+			let m = p.start();
+			p.bump();
+			expr(p);
+			m.complete(p, ExprImport)
+		} else if p.at(OpMinus) || p.at(OpNot) || p.at(OpBitNegate) {
+			let op = match p.peek().unwrap() {
+				OpMinus => UnaryOperator::Minus,
+				OpNot => UnaryOperator::Not,
+				OpBitNegate => UnaryOperator::BitNegate,
+				_ => unreachable!(),
+			};
+			let ((), right_binding_power) = op.binding_power();
+
+			let m = p.start();
+			p.bump();
+			expr_binding_power(p, right_binding_power);
+			m.complete(p, UnaryOp)
+		} else if p.at(LParen) {
+			let m = p.start();
+			p.bump();
+			expr(p);
+			assert!(p.at(RParen));
+			p.bump();
+			m.complete(p, Parened)
+		} else {
+			p.error_with_no_skip();
+			return None;
+		},
+	)
+}
+
+type SyntaxNode = rowan::SyntaxNode<Lang>;
+#[allow(unused)]
+type SyntaxToken = rowan::SyntaxToken<Lang>;
+#[allow(unused)]
+type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
+
+impl Parse {
+	pub fn syntax(&self) -> SyntaxNode {
+		SyntaxNode::new_root(self.green_node.clone())
+	}
+}
+
+pub fn parse(input: &str) -> Parse {
+	let lexemes = lex(input);
+	let parser = Parser::new(&lexemes);
+	let events = parser.parse();
+	dbg!(&events);
+	let sink = Sink::new(events, &lexemes);
+
+	sink.finish()
+}
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__array_comp.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__array_comp.snap
@@ -0,0 +1,43 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "[a for a in [1, 2, 3]]\n"
+
+---
+Root@0..23
+  Array@0..23
+    SymbolLeftBracket@0..1 "["
+    ArrayElem@1..21
+      BodyDef@1..3
+        Ident@1..3
+          Ident@1..2 "a"
+          Whitespace@2..3 " "
+      CompspecFor@3..21
+        KeywordFor@3..6 "for"
+        Whitespace@6..7 " "
+        Ident@7..8 "a"
+        Whitespace@8..9 " "
+        OpIn@9..11 "in"
+        Whitespace@11..12 " "
+        Array@12..21
+          SymbolLeftBracket@12..13 "["
+          ArrayElem@13..14
+            BodyDef@13..14
+              Literal@13..14
+                Number@13..14 "1"
+          SymbolComma@14..15 ","
+          Whitespace@15..16 " "
+          ArrayElem@16..17
+            BodyDef@16..17
+              Literal@16..17
+                Number@16..17 "2"
+          SymbolComma@17..18 ","
+          Whitespace@18..19 " "
+          ArrayElem@19..20
+            BodyDef@19..20
+              Literal@19..20
+                Number@19..20 "3"
+          SymbolRightBracket@20..21 "]"
+    SymbolRightBracket@21..22 "]"
+    Whitespace@22..23 "\n"
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__array_comp_incompatible_with_multiple_elems.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__array_comp_incompatible_with_multiple_elems.snap
@@ -0,0 +1,58 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "[a for a in [1, 2, 3], b]\n"
+
+---
+Root@0..26
+  Array@0..26
+    SymbolLeftBracket@0..1 "["
+    ArrayElem@1..21
+      BodyDef@1..3
+        Ident@1..3
+          Ident@1..2 "a"
+          Whitespace@2..3 " "
+      CompspecFor@3..21
+        KeywordFor@3..6 "for"
+        Whitespace@6..7 " "
+        Ident@7..8 "a"
+        Whitespace@8..9 " "
+        OpIn@9..11 "in"
+        Whitespace@11..12 " "
+        Array@12..21
+          SymbolLeftBracket@12..13 "["
+          ArrayElem@13..14
+            BodyDef@13..14
+              Literal@13..14
+                Number@13..14 "1"
+          SymbolComma@14..15 ","
+          Whitespace@15..16 " "
+          ArrayElem@16..17
+            BodyDef@16..17
+              Literal@16..17
+                Number@16..17 "2"
+          SymbolComma@17..18 ","
+          Whitespace@18..19 " "
+          ArrayElem@19..20
+            BodyDef@19..20
+              Literal@19..20
+                Number@19..20 "3"
+          SymbolRightBracket@20..21 "]"
+    SymbolComma@21..22 ","
+    Whitespace@22..23 " "
+    ArrayElem@23..24
+      BodyDef@23..24
+        Ident@23..24
+          Ident@23..24 "b"
+    SymbolRightBracket@24..25 "]"
+    Whitespace@25..26 "\n"
+===
+Custom { error: "compspec may only be used if there is only one array element", range: 3..21 }
+===
+  × syntax error
+   ╭────
+ 1 │ [a for a in [1, 2, 3], b]
+   ·    ─────────┬────────
+   ·             ╰── compspec may only be used if there is only one array element
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__empty.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__empty.snap
@@ -0,0 +1,18 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: " "
+
+---
+Root@0..1
+  Whitespace@0..1 " "
+===
+Missing { expected: Named("value"), offset: 1 }
+===
+  × syntax error
+   ╭────
+ 1 │  
+   ·  ▲
+   ·  ╰── missing value
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__function.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__function.snap
@@ -0,0 +1,34 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "function(a, b = 1) a + b\n"
+
+---
+Root@0..25
+  FunctionDef@0..25
+    KeywordFunction@0..8 "function"
+    LParen@8..9 "("
+    DefPositionalArg@9..10
+      Ident@9..10 "a"
+    SymbolComma@10..11 ","
+    Whitespace@11..12 " "
+    DefNamedArg@12..17
+      Ident@12..13 "b"
+      Whitespace@13..14 " "
+      SymbolAssign@14..15 "="
+      Whitespace@15..16 " "
+      Literal@16..17
+        Number@16..17 "1"
+    RParen@17..18 ")"
+    Whitespace@18..19 " "
+    BodyDef@19..25
+      BinOp@19..25
+        Ident@19..21
+          Ident@19..20 "a"
+          Whitespace@20..21 " "
+        OpPlus@21..22 "+"
+        Whitespace@22..23 " "
+        Ident@23..25
+          Ident@23..24 "b"
+          Whitespace@24..25 "\n"
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__function_error_body.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__function_error_body.snap
@@ -0,0 +1,29 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "function(a, b)\n"
+
+---
+Root@0..15
+  FunctionDef@0..15
+    KeywordFunction@0..8 "function"
+    LParen@8..9 "("
+    DefPositionalArg@9..10
+      Ident@9..10 "a"
+    SymbolComma@10..11 ","
+    Whitespace@11..12 " "
+    DefPositionalArg@12..13
+      Ident@12..13 "b"
+    RParen@13..14 ")"
+    Whitespace@14..15 "\n"
+    BodyDef@15..15
+===
+Missing { expected: Named("value"), offset: 14 }
+===
+  × syntax error
+   ╭────
+ 1 │ function(a, b)
+   ·               ▲
+   ·               ╰── missing value
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__function_error_no_value.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__function_error_no_value.snap
@@ -0,0 +1,41 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "function(a, b = ) a + b\n"
+
+---
+Root@0..24
+  FunctionDef@0..24
+    KeywordFunction@0..8 "function"
+    LParen@8..9 "("
+    DefPositionalArg@9..10
+      Ident@9..10 "a"
+    SymbolComma@10..11 ","
+    Whitespace@11..12 " "
+    DefNamedArg@12..16
+      Ident@12..13 "b"
+      Whitespace@13..14 " "
+      SymbolAssign@14..15 "="
+      Whitespace@15..16 " "
+    RParen@16..17 ")"
+    Whitespace@17..18 " "
+    BodyDef@18..24
+      BinOp@18..24
+        Ident@18..20
+          Ident@18..19 "a"
+          Whitespace@19..20 " "
+        OpPlus@20..21 "+"
+        Whitespace@21..22 " "
+        Ident@22..24
+          Ident@22..23 "b"
+          Whitespace@23..24 "\n"
+===
+Missing { expected: Named("value"), offset: 15 }
+===
+  × syntax error
+   ╭────
+ 1 │ function(a, b = ) a + b
+   ·                ▲
+   ·                ╰── missing value
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__function_error_rparen.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__function_error_rparen.snap
@@ -0,0 +1,30 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "function(a, b\n"
+
+---
+Root@0..14
+  FunctionDef@0..14
+    KeywordFunction@0..8 "function"
+    LParen@8..9 "("
+    DefPositionalArg@9..10
+      Ident@9..10 "a"
+    SymbolComma@10..11 ","
+    Whitespace@11..12 " "
+    DefPositionalArg@12..14
+      Ident@12..13 "b"
+      Whitespace@13..14 "\n"
+    BodyDef@14..14
+===
+Missing { expected: Unnamed(RParen), offset: 13 }
+Missing { expected: Named("value"), offset: 13 }
+===
+  × syntax error
+   ╭────
+ 1 │ function(a, b
+   ·              ▲
+   ·              │╰── missing value
+   ·              ╰── missing RParen
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__local_no_value_recovery.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__local_no_value_recovery.snap
@@ -0,0 +1,47 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "local a =\nlocal b = 3;\n1\n"
+
+---
+Root@0..25
+  Local@0..25
+    KeywordLocal@0..5 "local"
+    Whitespace@5..6 " "
+    Ident@6..7 "a"
+    Whitespace@7..8 " "
+    SymbolAssign@8..9 "="
+    Whitespace@9..10 "\n"
+    Local@10..25
+      KeywordLocal@10..15 "local"
+      Whitespace@15..16 " "
+      Ident@16..17 "b"
+      Whitespace@17..18 " "
+      SymbolAssign@18..19 "="
+      Whitespace@19..20 " "
+      Literal@20..21
+        Number@20..21 "3"
+      SymbolSemi@21..22 ";"
+      Whitespace@22..23 "\n"
+      BodyDef@23..25
+        Literal@23..25
+          Number@23..24 "1"
+          Whitespace@24..25 "\n"
+    BodyDef@25..25
+===
+Missing { expected: Unnamed(SymbolSemi), offset: 24 }
+Custom { error: "unusal local placement, missing ';' ?", range: 8..9 }
+Missing { expected: Named("value"), offset: 24 }
+===
+  × syntax error
+   ╭─[1:1]
+ 1 │ local a =
+   ·         ┬
+   ·         ╰── unusal local placement, missing ';' ?
+ 2 │ local b = 3;
+ 3 │ 1
+   ·  ▲
+   ·  │╰── missing value
+   ·  ╰── missing SymbolSemi
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__local_no_value_recovery.snap.newdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__local_no_value_recovery.snap.new
@@ -0,0 +1,43 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "local a =\nlocal b = 3;\n1\n"
+
+---
+Root@0..25
+  Local@0..25
+    KeywordLocal@0..5 "local"
+    Whitespace@5..6 " "
+    Ident@6..7 "a"
+    Whitespace@7..8 " "
+    SymbolAssign@8..9 "="
+    Whitespace@9..10 "\n"
+    Local@10..25
+      KeywordLocal@10..15 "local"
+      Whitespace@15..16 " "
+      Ident@16..17 "b"
+      Whitespace@17..18 " "
+      SymbolAssign@18..19 "="
+      Whitespace@19..20 " "
+      Literal@20..21
+        Number@20..21 "3"
+      SymbolSemi@21..22 ";"
+      Whitespace@22..23 "\n"
+      BodyDef@23..25
+        Literal@23..25
+          Number@23..24 "1"
+          Whitespace@24..25 "\n"
+    BodyDef@25..25
+===
+Missing { expected: Unnamed(SymbolSemi), offset: 24 }
+Missing { expected: Named("value"), offset: 24 }
+===
+  × syntax error
+   ╭─[2:1]
+ 2 │ local b = 3;
+ 3 │ 1
+   ·  ▲
+   ·  │╰── missing value
+   ·  ╰── missing SymbolSemi
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__local_novalue.snapdiffbeforeafterboth

no changes

addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__named_before_positional.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__named_before_positional.snap
@@ -0,0 +1,63 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "a(1, 2, b=4, 3, 5, k = 12, 6)\n"
+
+---
+Root@0..30
+  FunctionCall@0..30
+    Ident@0..1
+      Ident@0..1 "a"
+    LParen@1..2 "("
+    DefPositionalArg@2..3
+      Literal@2..3
+        Number@2..3 "1"
+    SymbolComma@3..4 ","
+    Whitespace@4..5 " "
+    DefPositionalArg@5..6
+      Literal@5..6
+        Number@5..6 "2"
+    SymbolComma@6..7 ","
+    Whitespace@7..8 " "
+    DefNamedArg@8..11
+      Ident@8..9 "b"
+      SymbolAssign@9..10 "="
+      Literal@10..11
+        Number@10..11 "4"
+    SymbolComma@11..12 ","
+    Whitespace@12..13 " "
+    ErrorPositionalAfterNamed@13..28
+      DefPositionalArg@13..14
+        Literal@13..14
+          Number@13..14 "3"
+      SymbolComma@14..15 ","
+      Whitespace@15..16 " "
+      DefPositionalArg@16..17
+        Literal@16..17
+          Number@16..17 "5"
+      SymbolComma@17..18 ","
+      Whitespace@18..19 " "
+      DefNamedArg@19..25
+        Ident@19..20 "k"
+        Whitespace@20..21 " "
+        SymbolAssign@21..22 "="
+        Whitespace@22..23 " "
+        Literal@23..25
+          Number@23..25 "12"
+      SymbolComma@25..26 ","
+      Whitespace@26..27 " "
+      DefPositionalArg@27..28
+        Literal@27..28
+          Number@27..28 "6"
+    RParen@28..29 ")"
+    Whitespace@29..30 "\n"
+===
+Custom { error: "positional arguments can't be placed after named", range: 13..28 }
+===
+  × syntax error
+   ╭────
+ 1 │ a(1, 2, b=4, 3, 5, k = 12, 6)
+   ·              ───────┬───────
+   ·                     ╰── positional arguments can't be placed after named
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__no_lhs.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__no_lhs.snap
@@ -0,0 +1,23 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "+ 2\n"
+
+---
+Root@0..4
+  OpPlus@0..1 "+"
+  Whitespace@1..2 " "
+  Number@2..3 "2"
+  Whitespace@3..4 "\n"
+===
+Missing { expected: Named("value"), offset: 0 }
+Custom { error: "unexpected input after expression", range: 0..3 }
+===
+  × syntax error
+   ╭────
+ 1 │ + 2
+   · ▲─┬─
+   · │╰── unexpected input after expression
+   · ╰── missing value
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__no_operator.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__no_operator.snap
@@ -0,0 +1,22 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "2 2\n"
+
+---
+Root@0..4
+  Literal@0..2
+    Number@0..1 "2"
+    Whitespace@1..2 " "
+  Number@2..3 "2"
+  Whitespace@3..4 "\n"
+===
+Custom { error: "unexpected input after expression", range: 2..3 }
+===
+  × syntax error
+   ╭────
+ 1 │ 2 2
+   ·   ┬
+   ·   ╰── unexpected input after expression
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__no_rhs.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__no_rhs.snap
@@ -0,0 +1,23 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "a +\n"
+
+---
+Root@0..4
+  BinOp@0..4
+    Ident@0..2
+      Ident@0..1 "a"
+      Whitespace@1..2 " "
+    OpPlus@2..3 "+"
+    Whitespace@3..4 "\n"
+===
+Missing { expected: Named("value"), offset: 3 }
+===
+  × syntax error
+   ╭────
+ 1 │ a +
+   ·    ▲
+   ·    ╰── missing value
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__wrong_field_end.snap.newdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/snapshots/jrsonnet_rowan_parser__tests__wrong_field_end.snap.new
@@ -0,0 +1,41 @@
+---
+source: crates/jrsonnet-rowan-parser/src/lib.rs
+assertion_line: 88
+expression: "{\n\ta: 1;\n\tb: 2;\n}\n"
+
+---
+Root@0..18
+  Object@0..7
+    SymbolLeftBrace@0..1 "{"
+    Whitespace@1..3 "\n\t"
+    Field@3..7
+      Ident@3..4 "a"
+      SymbolColon@4..5 ":"
+      Whitespace@5..6 " "
+      Literal@6..7
+        Number@6..7 "1"
+  SymbolSemi@7..8 ";"
+  Whitespace@8..10 "\n\t"
+  Ident@10..11 "b"
+  SymbolColon@11..12 ":"
+  Whitespace@12..13 " "
+  Number@13..14 "2"
+  SymbolSemi@14..15 ";"
+  Whitespace@15..16 "\n"
+  SymbolRightBrace@16..17 "}"
+  Whitespace@17..18 "\n"
+===
+Missing { expected: Unnamed(SymbolRightBrace), offset: 7 }
+Custom { error: "unexpected input after expression", range: 7..17 }
+===
+  × syntax error
+   ╭─[1:1]
+ 1 │     {
+ 2 │ ╭─▶ 	a: 1;
+   · ││     ▲
+   · ││     ╰── missing SymbolRightBrace
+ 3 │ │   	b: 2;
+ 4 │ ├─▶ }
+   · ╰──── unexpected input after expression
+   ╰────
+
addedcrates/jrsonnet-rowan-parser/src/string_block.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/string_block.rs
@@ -0,0 +1,221 @@
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum StringBlockToken {
+	Valid,
+	UnexpectedEndOfString,
+	MissingTextBlockNewLine,
+	MissingTextBlockTermination,
+	MissingTextBlockIndent,
+}
+
+use std::ops::Range;
+
+use StringBlockToken::*;
+
+use crate::lex::SyntaxKind;
+
+pub fn lex_str_block_test<'a>(lex: &mut logos::Lexer<'a, SyntaxKind>) {
+	lex_str_block(lex);
+}
+
+fn lex_str_block<'a>(lex: &mut logos::Lexer<'a, SyntaxKind>) -> StringBlockToken {
+	struct Context<'a> {
+		source: &'a str,
+		index: usize,
+		offset: usize,
+	}
+
+	impl<'a> Context<'a> {
+		fn rest(&self) -> &'a str {
+			&self.source[self.index..]
+		}
+
+		fn next(&mut self) -> Option<char> {
+			if self.index == self.source.len() {
+				return None;
+			}
+
+			match self.rest().chars().next() {
+				None => None,
+				Some(c) => {
+					self.index += c.len_utf8();
+					Some(c)
+				}
+			}
+		}
+
+		fn peek(&self) -> Option<char> {
+			if self.index == self.source.len() {
+				return None;
+			}
+
+			self.rest().chars().next()
+		}
+
+		fn eat_while(&mut self, f: impl Fn(char) -> bool) -> usize {
+			if self.index == self.source.len() {
+				return 0;
+			}
+
+			let next_char = self.rest().char_indices().find(|(_, c)| !f(*c));
+
+			match next_char {
+				None => {
+					let diff = self.source.len() - self.index;
+					self.index = self.source.len();
+					diff
+				}
+				Some((idx, _)) => {
+					self.index += idx;
+					idx
+				}
+			}
+		}
+
+		fn skip(&mut self, len: usize) {
+			self.index = match self.index + len {
+				n if n > self.source.len() => self.source.len(),
+				n => n,
+			};
+		}
+
+		fn pos(&self) -> Range<usize> {
+			if self.index == self.source.len() {
+				self.offset + self.index..self.offset + self.index
+			} else {
+				// TODO: char size
+				self.offset + self.index..self.offset + self.index + 1
+			}
+		}
+	}
+
+	// Check that b has at least the same whitespace prefix as a and returns the
+	// amount of this whitespace, otherwise returns 0.  If a has no whitespace
+	// prefix than return 0.
+	fn check_whitespace(a: &str, b: &str) -> usize {
+		let a = a.as_bytes();
+		let b = b.as_bytes();
+
+		for i in 0..a.len() {
+			if a[i] != b' ' && a[i] != b'\t' {
+				// a has run out of whitespace and b matched up to this point. Return result.
+				return i;
+			}
+
+			if i >= b.len() {
+				// We ran off the edge of b while a still has whitespace. Return 0 as failure.
+				return 0;
+			}
+
+			if a[i] != b[i] {
+				// a has whitespace but b does not. Return 0 as failure.
+				return 0;
+			}
+		}
+
+		// We ran off the end of a and b kept up
+		a.len()
+	}
+
+	fn guess_token_end_and_bump<'a>(lex: &mut logos::Lexer<'a, SyntaxKind>, ctx: &Context<'a>) {
+		let end_index = ctx
+			.rest()
+			.find("|||")
+			.map(|v| v + 3)
+			.unwrap_or_else(|| ctx.rest().len());
+		lex.bump(ctx.index + end_index);
+	}
+
+	debug_assert_eq!(lex.slice(), "|||");
+	let mut ctx = Context {
+		source: lex.remainder(),
+		index: 0,
+		offset: lex.span().end,
+	};
+
+	// Skip whitespaces
+	ctx.eat_while(|r| r == ' ' || r == '\t' || r == '\r');
+
+	// Skip \n
+	match ctx.next() {
+		Some('\n') => (),
+		None => {
+			guess_token_end_and_bump(lex, &ctx);
+			return UnexpectedEndOfString;
+		}
+		// Text block requires new line after |||.
+		Some(_) => {
+			guess_token_end_and_bump(lex, &ctx);
+			return MissingTextBlockNewLine;
+		}
+	}
+
+	// Process leading blank lines before calculating string block indent
+	while let Some('\n') = ctx.peek() {
+		ctx.next();
+	}
+
+	let mut num_whitespace = check_whitespace(ctx.rest(), ctx.rest());
+	let str_block_indent = &ctx.rest()[..num_whitespace];
+
+	if num_whitespace == 0 {
+		// Text block's first line must start with whitespace
+		guess_token_end_and_bump(lex, &ctx);
+		return MissingTextBlockIndent;
+	}
+
+	loop {
+		debug_assert_ne!(num_whitespace, 0, "Unexpected value for num_whitespace");
+		ctx.skip(num_whitespace);
+
+		loop {
+			match ctx.next() {
+				None => {
+					guess_token_end_and_bump(lex, &ctx);
+					return UnexpectedEndOfString;
+				}
+				Some('\n') => break,
+				Some(_) => (),
+			}
+		}
+
+		// Skip any blank lines
+		while let Some('\n') = ctx.peek() {
+			ctx.next();
+		}
+
+		// Look at the next line
+		num_whitespace = check_whitespace(str_block_indent, ctx.rest());
+		if num_whitespace == 0 {
+			// End of the text block
+			let mut term_indent = String::with_capacity(num_whitespace);
+			loop {
+				match ctx.peek() {
+					Some(' ') | Some('\t') => {
+						term_indent.push(ctx.next().unwrap());
+					}
+					_ => break,
+				}
+			}
+
+			if !ctx.rest().starts_with("|||") {
+				// Text block not terminated with |||
+				let pos = ctx.pos();
+				if pos.len() == 0 {
+					// eof
+					lex.bump(ctx.index);
+					return UnexpectedEndOfString;
+				}
+
+				guess_token_end_and_bump(lex, &ctx);
+				return MissingTextBlockTermination;
+			}
+
+			// Skip '|||'
+			ctx.skip(3);
+			break;
+		}
+	}
+
+	lex.bump(ctx.index);
+	Valid
+}
addedcrates/jrsonnet-rowan-parser/src/token_set.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/token_set.rs
@@ -0,0 +1,31 @@
+use crate::lex::SyntaxKind;
+
+#[derive(Clone, Copy, Default)]
+pub struct TokenSet(u64);
+
+impl TokenSet {
+	pub const EMPTY: Self = Self(0);
+	pub const ALL: Self = Self(u64::MAX);
+
+	pub const fn new(kinds: &[SyntaxKind]) -> TokenSet {
+		let mut res = 0u64;
+		let mut i = 0;
+		while i < kinds.len() {
+			res |= mask(kinds[i]);
+			i += 1
+		}
+		TokenSet(res)
+	}
+
+	pub const fn union(self, other: TokenSet) -> TokenSet {
+		TokenSet(self.0 | other.0)
+	}
+
+	pub const fn contains(&self, kind: SyntaxKind) -> bool {
+		self.0 & mask(kind) != 0
+	}
+}
+
+const fn mask(kind: SyntaxKind) -> u64 {
+	1u64 << (kind as usize)
+}
addedcrates/jrsonnet-rowan-parser/src/unary.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-rowan-parser/src/unary.rs
@@ -0,0 +1,15 @@
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum UnaryOperator {
+	Minus,
+	Not,
+	BitNegate,
+}
+impl UnaryOperator {
+	pub fn binding_power(&self) -> ((), u8) {
+		match self {
+			UnaryOperator::Minus => ((), 20),
+			UnaryOperator::Not => ((), 20),
+			UnaryOperator::BitNegate => ((), 20),
+		}
+	}
+}
addedjrsonnet-lsp/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/jrsonnet-lsp/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "jrsonnet-lsp"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.48"
+jrsonnet-evaluator = { path = "../jrsonnet-evaluator" }
+jrsonnet-parser = { path = "../jrsonnet-parser" }
+lsp-server = "0.5.2"
+lsp-types = "0.92.0"
+serde = "1.0.130"
+serde_json = "1.0.71"
addedjrsonnet-lsp/src/main.rsdiffbeforeafterboth
--- /dev/null
+++ b/jrsonnet-lsp/src/main.rs
@@ -0,0 +1,211 @@
+use std::{
+	collections::HashMap,
+	fs::File,
+	path::{Path, PathBuf},
+	str::FromStr,
+};
+
+use jrsonnet_evaluator::{EvaluationState, FileImportResolver, Val};
+use jrsonnet_parser::{ExprLocation, ParserSettings};
+use lsp_server::{Connection, ErrorCode, Message, Request, RequestId, Response};
+use lsp_types::{
+	notification::{DidChangeTextDocument, DidOpenTextDocument, Notification},
+	request::{DocumentLinkRequest, HoverRequest},
+	CompletionOptions, DidChangeTextDocumentParams, DidOpenTextDocumentParams, DocumentLink,
+	DocumentLinkOptions, Hover, HoverContents, MarkupContent, MarkupKind, ServerCapabilities,
+	TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, Url,
+	WorkDoneProgressOptions,
+};
+
+use std::io::Write;
+
+fn main() {
+	let mut log = File::create("test").unwrap();
+	writeln!(log, "start").unwrap();
+	let (connection, io_threads) = Connection::stdio();
+	let capabilities = serde_json::to_value(&ServerCapabilities {
+		completion_provider: Some(CompletionOptions::default()),
+		definition_provider: Some(lsp_types::OneOf::Left(true)),
+		document_link_provider: Some(DocumentLinkOptions {
+			resolve_provider: Some(false),
+			work_done_progress_options: WorkDoneProgressOptions::default(),
+		}),
+		hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
+		text_document_sync: Some(TextDocumentSyncCapability::Options(
+			TextDocumentSyncOptions {
+				change: Some(TextDocumentSyncKind::FULL),
+				open_close: Some(true),
+				..TextDocumentSyncOptions::default()
+			},
+		)),
+		..ServerCapabilities::default()
+	})
+	.expect("failed to convert capabilities to json");
+
+	connection
+		.initialize(capabilities)
+		.expect("failed to initialize connection");
+
+	writeln!(log, "initialized").unwrap();
+
+	main_loop(&mut log, &connection).expect("main loop failed");
+
+	io_threads.join().expect("failed to join io_threads");
+}
+fn main_loop(log: &mut File, connection: &Connection) -> anyhow::Result<()> {
+	let mut es = EvaluationState::default();
+	es.set_import_resolver(Box::new(FileImportResolver::default()));
+
+	let reply = |response: Response| {
+		connection
+			.sender
+			.send(Message::Response(response))
+			.expect("failed to respond");
+	};
+
+	for msg in &connection.receiver {
+		match msg {
+			Message::Response(_) => (),
+			Message::Request(req) => {
+				if connection.handle_shutdown(&req)? {
+					return Ok(());
+				}
+				if let Some((id, params)) = cast::<DocumentLinkRequest>(&req) {
+					reply(Response::new_ok(id, <Vec<DocumentLink>>::new()));
+				} else if let Some((id, params)) = cast::<HoverRequest>(&req) {
+					let pos = params
+						.text_document_position_params
+						.text_document
+						.uri
+						.path();
+					let buf = PathBuf::from_str(pos).unwrap();
+					let pos = es
+						.map_from_source_location(
+							&buf,
+							params.text_document_position_params.position.line as usize + 1,
+							params.text_document_position_params.position.character as usize + 1,
+						)
+						.unwrap();
+					let el = ExprLocation(buf.clone().into(), pos as usize, pos as usize);
+					let es2 = es.clone();
+				// reply(Response::new_ok(
+				// 	id,
+				// 	Some(Hover {
+				// 		range: None,
+				// 		contents: HoverContents::Markup(MarkupContent {
+				// 			kind: MarkupKind::Markdown,
+				// 			value: es
+				// 				.run_in_state_with_breakpoint(el, move || {
+				// 					es2.reset_evaluation_state(&buf);
+				// 					es2.import_file(&PathBuf::new(), &buf)?
+				// 						.to_string()
+				// 						.map(|_| ())
+				// 				})
+				// 				.unwrap()
+				// 				.unwrap_or_else(|| Val::Null)
+				// 				.value_type()
+				// 				.to_string(),
+				// 		}),
+				// 	}),
+				// ));
+				} else
+				/*
+				if let Some((id, params)) = cast::<DocumentLinkRequest>(&req) {
+					 let links = handle_links(&files, params).unwrap_or_default();
+					 reply(Response::new_ok(id, links));
+				} else if let Some((id, params)) = cast::<GotoDefinition>(&req) {
+					 if let Some(loc) = handle_goto(&files, params) {
+						  reply(Response::new_ok(id, loc))
+					 } else {
+						  reply(Response::new_ok(id, ()))
+					 }
+				} else if let Some((id, params)) = cast::<HoverRequest>(&req) {
+					 match handle_hover(&files, params) {
+						  Some((range, markdown)) => {
+								reply(Response::new_ok(
+									 id,
+									 Hover {
+										  contents: HoverContents::Markup(MarkupContent {
+												kind: MarkupKind::Markdown,
+												value: markdown,
+										  }),
+										  range,
+									 },
+								));
+						  }
+						  None => {
+								reply(Response::new_ok(id, ()));
+						  }
+					 }
+				} else if let Some((id, params)) = cast::<Completion>(&req) {
+					 let completions = handle_completion(&files, params.text_document_position)
+						  .unwrap_or_default();
+					 reply(Response::new_ok(id, completions));
+				} else
+				*/
+				{
+					reply(Response::new_err(
+						req.id,
+						ErrorCode::MethodNotFound as i32,
+						format!("unrecognized request {}", req.method),
+					))
+				}
+			}
+			Message::Notification(req) => {
+				let mut handle = |text: String, uri: Url| {
+					writeln!(log, "updated file: {:?}", uri).unwrap();
+					let path = match PathBuf::from_str(uri.path()) {
+						Ok(x) => x,
+						Err(_) => return,
+					};
+					let parsed = match jrsonnet_parser::parse(
+						&text,
+						&ParserSettings {
+							file_name: path.clone().into(),
+						},
+					) {
+						Ok(v) => v,
+						Err(e) => {
+							writeln!(log, "fuck D: {:?}", e).unwrap();
+							return;
+							// connection.sender.send(Message::Notification(Notification::new_err(req.id, ErrorCode::ParseError as i32, format!("Fuck D: {:?}", e))))
+						}
+					};
+					es.add_parsed_file(path.into(), text.into(), parsed)
+						.unwrap();
+					writeln!(log, "parsed: {:?}", uri).unwrap();
+				};
+
+				match &*req.method {
+					DidOpenTextDocument::METHOD => {
+						let params: DidOpenTextDocumentParams =
+							match serde_json::from_value(req.params) {
+								Ok(x) => x,
+								Err(_) => continue,
+							};
+						handle(params.text_document.text, params.text_document.uri);
+					}
+					DidChangeTextDocument::METHOD => {
+						let params: DidChangeTextDocumentParams =
+							match serde_json::from_value(req.params) {
+								Ok(x) => x,
+								Err(_) => continue,
+							};
+						for change in params.content_changes.into_iter() {
+							handle(change.text, params.text_document.uri.clone());
+						}
+					}
+					_ => continue,
+				}
+			}
+		}
+	}
+	Ok(())
+}
+fn cast<R>(req: &Request) -> Option<(RequestId, R::Params)>
+where
+	R: lsp_types::request::Request,
+	R::Params: serde::de::DeserializeOwned,
+{
+	req.clone().extract(R::METHOD).ok()
+}