diff --git a/crates/simplexpr/Cargo.lock b/crates/simplexpr/Cargo.lock new file mode 100644 index 0000000..774d4b5 --- /dev/null +++ b/crates/simplexpr/Cargo.lock @@ -0,0 +1,656 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "beef" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6736e2428df2ca2848d846c43e88745121a6654696e349ce0054a420815a7409" + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "ena" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" +dependencies = [ + "log", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "insta" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1b21a2971cea49ca4613c0e9fe8225ecaf5de64090fddc6002284726e9244" +dependencies = [ + "console", + "lazy_static", + "serde", + "serde_json", + "serde_yaml", + "similar", + "uuid", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lalrpop" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988" +dependencies = [ + "ascii-canvas", + "atty", + "bit-set", + "diff", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278" +dependencies = [ + "regex", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "logos" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427e2abca5be13136da9afdbf874e6b34ad9001dd70f2b103b083a85daa7b345" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56a7d287fd2ac3f75b11f19a1c8a874a7d55744bd91f7a1b3e7cf87d4343c36d" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax", + "syn", + "utf8-ranges", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + +[[package]] +name = "simplexpr" +version = "0.1.0" +dependencies = [ + "insta", + "itertools", + "lalrpop", + "lalrpop-util", + "logos", + "maplit", + "regex", + "serde", + "serde_json", + "strum", + "thiserror", +] + +[[package]] +name = "siphasher" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[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 = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/crates/simplexpr/Cargo.toml b/crates/simplexpr/Cargo.toml new file mode 100644 index 0000000..df354ef --- /dev/null +++ b/crates/simplexpr/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "simplexpr" +version = "0.1.0" +edition = "2018" +authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"] + + +build = "build.rs" + +[dependencies] +lalrpop-util = "0.19.5" +regex = "1" +itertools = "0.10" +thiserror = "1.0" +maplit = "1.0" +logos = "0.12" + +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" + +strum = { version = "0.21", features = ["derive"] } + + + +[build-dependencies] +lalrpop = "0.19.5" + +[dev-dependencies] +insta = "1.7" diff --git a/crates/simplexpr/README.md b/crates/simplexpr/README.md new file mode 100644 index 0000000..4a60e3d --- /dev/null +++ b/crates/simplexpr/README.md @@ -0,0 +1,6 @@ +# simplexpr + +simplexpr is a parser and interpreter for a simple expression syntax that can be embedded into other applications or crates. +It is being developed to be used in [eww](https://github.com/elkowar/eww), but may also other uses. + +For now, this is highly experimental, unstable, and ugly. You most definitely do not want to use this crate. diff --git a/crates/simplexpr/build.rs b/crates/simplexpr/build.rs new file mode 100644 index 0000000..cbc2c25 --- /dev/null +++ b/crates/simplexpr/build.rs @@ -0,0 +1,4 @@ +extern crate lalrpop; +fn main() { + lalrpop::Configuration::new().log_verbose().process_current_dir().unwrap(); +} diff --git a/crates/simplexpr/rust-toolchain b/crates/simplexpr/rust-toolchain new file mode 100644 index 0000000..bf867e0 --- /dev/null +++ b/crates/simplexpr/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/crates/simplexpr/rustfmt.toml b/crates/simplexpr/rustfmt.toml new file mode 100644 index 0000000..edce9c8 --- /dev/null +++ b/crates/simplexpr/rustfmt.toml @@ -0,0 +1,14 @@ +unstable_features = true +fn_single_line = false +max_width = 130 +reorder_impl_items = true +merge_imports = true +normalize_comments = true +use_field_init_shorthand = true +#wrap_comments = true +combine_control_expr = false +condense_wildcard_suffixes = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +use_small_heuristics = "Max" diff --git a/crates/simplexpr/src/EvalError.rs b/crates/simplexpr/src/EvalError.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/simplexpr/src/ast.rs b/crates/simplexpr/src/ast.rs new file mode 100644 index 0000000..519b0d3 --- /dev/null +++ b/crates/simplexpr/src/ast.rs @@ -0,0 +1,93 @@ +use crate::dynval::DynVal; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +/// stores the left and right end of a span, and a given file identifier. +#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] +pub struct Span(pub usize, pub usize, pub usize); + +impl std::fmt::Display for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}..{}", self.0, self.1) + } +} + +impl std::fmt::Debug for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}..{}", self.0, self.1) + } +} + +#[rustfmt::skip] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, strum::EnumString, strum::Display)] +pub enum BinOp { + #[strum(serialize = "+") ] Plus, + #[strum(serialize = "-") ] Minus, + #[strum(serialize = "*") ] Times, + #[strum(serialize = "/") ] Div, + #[strum(serialize = "%") ] Mod, + #[strum(serialize = "==")] Equals, + #[strum(serialize = "!=")] NotEquals, + #[strum(serialize = "&&")] And, + #[strum(serialize = "||")] Or, + #[strum(serialize = ">") ] GT, + #[strum(serialize = "<") ] LT, + #[strum(serialize = "?:")] Elvis, + #[strum(serialize = "=~")] RegexMatch, +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, strum::EnumString, strum::Display)] +pub enum UnaryOp { + #[strum(serialize = "!")] + Not, +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SimplExpr { + Literal(Span, DynVal), + VarRef(Span, String), + BinOp(Span, Box, BinOp, Box), + UnaryOp(Span, UnaryOp, Box), + IfElse(Span, Box, Box, Box), + JsonAccess(Span, Box, Box), + FunctionCall(Span, String, Vec), +} + +impl std::fmt::Display for SimplExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SimplExpr::VarRef(_, x) => write!(f, "{}", x), + SimplExpr::Literal(_, x) => write!(f, "\"{}\"", x), + SimplExpr::BinOp(_, l, op, r) => write!(f, "({} {} {})", l, op, r), + SimplExpr::UnaryOp(_, op, x) => write!(f, "{}{}", op, x), + SimplExpr::IfElse(_, a, b, c) => write!(f, "(if {} then {} else {})", a, b, c), + SimplExpr::JsonAccess(_, value, index) => write!(f, "{}[{}]", value, index), + SimplExpr::FunctionCall(_, function_name, args) => { + write!(f, "{}({})", function_name, args.iter().join(", ")) + } + } + } +} +impl SimplExpr { + pub fn literal(span: Span, s: String) -> Self { + Self::Literal(span, DynVal(s, Some(span))) + } + + pub fn span(&self) -> Span { + match self { + SimplExpr::Literal(span, _) => *span, + SimplExpr::VarRef(span, _) => *span, + SimplExpr::BinOp(span, ..) => *span, + SimplExpr::UnaryOp(span, ..) => *span, + SimplExpr::IfElse(span, ..) => *span, + SimplExpr::JsonAccess(span, ..) => *span, + SimplExpr::FunctionCall(span, ..) => *span, + } + } +} + +impl std::fmt::Debug for SimplExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} diff --git a/crates/simplexpr/src/dynval.rs b/crates/simplexpr/src/dynval.rs new file mode 100644 index 0000000..2ee7c5a --- /dev/null +++ b/crates/simplexpr/src/dynval.rs @@ -0,0 +1,218 @@ +use crate::ast::Span; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::{fmt, iter::FromIterator, str::FromStr}; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +#[error("Failed to turn {value} into a value of type {target_type}")] +pub struct ConversionError { + pub value: DynVal, + pub target_type: &'static str, + pub source: Option>, +} + +impl ConversionError { + fn new(value: DynVal, target_type: &'static str, source: impl std::error::Error + 'static) -> Self { + ConversionError { value, target_type, source: Some(Box::new(source)) } + } + + pub fn span(&self) -> Option { + self.value.1 + } +} + +#[derive(Clone, Deserialize, Serialize, Default, Eq)] +pub struct DynVal(pub String, pub Option); + +impl From for DynVal { + fn from(s: String) -> Self { + DynVal(s, None) + } +} + +impl fmt::Display for DynVal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} +impl fmt::Debug for DynVal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "\"{}\"", self.0) + } +} + +/// Manually implement equality, to allow for values in different formats (i.e. "1" and "1.0") to still be considered as equal. +impl std::cmp::PartialEq for DynVal { + fn eq(&self, other: &Self) -> bool { + if let (Ok(a), Ok(b)) = (self.as_f64(), other.as_f64()) { + a == b + } else { + self.0 == other.0 + } + } +} + +impl FromIterator for DynVal { + fn from_iter>(iter: T) -> Self { + DynVal(iter.into_iter().join(""), None) + } +} + +impl std::str::FromStr for DynVal { + type Err = ConversionError; + + /// parses the value, trying to turn it into a number and a boolean first, + /// before deciding that it is a string. + fn from_str(s: &str) -> Result { + Ok(DynVal::from_string(s.to_string())) + } +} + +pub trait FromDynVal: Sized { + type Err; + fn from_dynval(x: &DynVal) -> std::result::Result; +} + +impl> FromDynVal for T { + type Err = E; + + fn from_dynval(x: &DynVal) -> std::result::Result { + x.0.parse() + } +} + +macro_rules! impl_dynval_from { + ($($t:ty),*) => { + $(impl From<$t> for DynVal { + fn from(x: $t) -> Self { DynVal(x.to_string(), None) } + })* + }; +} + +impl_dynval_from!(bool, i32, u32, f32, u8, f64, &str); + +impl From<&serde_json::Value> for DynVal { + fn from(v: &serde_json::Value) -> Self { + DynVal( + v.as_str() + .map(|x| x.to_string()) + .or_else(|| serde_json::to_string(v).ok()) + .unwrap_or_else(|| "".to_string()), + None, + ) + } +} + +impl DynVal { + pub fn at(self, span: Span) -> Self { + DynVal(self.0, Some(span)) + } + + pub fn span(&self) -> Option { + self.1 + } + + pub fn from_string(s: String) -> Self { + DynVal(s, None) + } + + pub fn read_as>(&self) -> std::result::Result { + T::from_dynval(self) + } + + pub fn into_inner(self) -> String { + self.0 + } + + /// This will never fail + pub fn as_string(&self) -> Result { + Ok(self.0.to_owned()) + } + + pub fn as_f64(&self) -> Result { + self.0.parse().map_err(|e| ConversionError::new(self.clone(), "f64", e)) + } + + pub fn as_i32(&self) -> Result { + self.0.parse().map_err(|e| ConversionError::new(self.clone(), "i32", e)) + } + + pub fn as_bool(&self) -> Result { + self.0.parse().map_err(|e| ConversionError::new(self.clone(), "bool", e)) + } + + pub fn as_duration(&self) -> Result { + use std::time::Duration; + let s = &self.0; + if s.ends_with("ms") { + Ok(Duration::from_millis( + s.trim_end_matches("ms").parse().map_err(|e| ConversionError::new(self.clone(), "integer", e))?, + )) + } else if s.ends_with('s') { + Ok(Duration::from_secs( + s.trim_end_matches('s').parse().map_err(|e| ConversionError::new(self.clone(), "integer", e))?, + )) + } else if s.ends_with('m') { + Ok(Duration::from_secs( + s.trim_end_matches('m').parse::().map_err(|e| ConversionError::new(self.clone(), "integer", e))? * 60, + )) + } else if s.ends_with('h') { + Ok(Duration::from_secs( + s.trim_end_matches('h').parse::().map_err(|e| ConversionError::new(self.clone(), "integer", e))? * 60 * 60, + )) + } else { + Err(ConversionError { value: self.clone(), target_type: "duration", source: None }) + } + } + + // pub fn as_vec(&self) -> Result> { + // match self.0.strip_prefix('[').and_then(|x| x.strip_suffix(']')) { + // Some(content) => { + // let mut items: Vec = content.split(',').map(|x: &str| x.to_string()).collect(); + // let mut removed = 0; + // for times_ran in 0..items.len() { + //// escapes `,` if there's a `\` before em + // if items[times_ran - removed].ends_with('\\') { + // items[times_ran - removed].pop(); + // let it = items.remove((times_ran + 1) - removed); + // items[times_ran - removed] += ","; + // items[times_ran - removed] += ⁢ + // removed += 1; + //} + // Ok(items) + //} + // None => Err(ConversionError { value: self.clone(), target_type: "vec", source: None }), + //} + + pub fn as_json_value(&self) -> Result { + serde_json::from_str::(&self.0) + .map_err(|e| ConversionError::new(self.clone(), "json-value", Box::new(e))) + } +} + +#[cfg(test)] +mod test { + // use super::*; + // use pretty_assertions::assert_eq; + //#[test] + // fn test_parse_vec() { + // assert_eq!(vec![""], parse_vec("[]".to_string()).unwrap(), "should be able to parse empty lists"); + // assert_eq!(vec!["hi"], parse_vec("[hi]".to_string()).unwrap(), "should be able to parse single element list"); + // assert_eq!( + // vec!["hi", "ho", "hu"], + // parse_vec("[hi,ho,hu]".to_string()).unwrap(), + //"should be able to parse three element list" + //); + // assert_eq!(vec!["hi,ho"], parse_vec("[hi\\,ho]".to_string()).unwrap(), "should be able to parse list with escaped comma"); + // assert_eq!( + // vec!["hi,ho", "hu"], + // parse_vec("[hi\\,ho,hu]".to_string()).unwrap(), + //"should be able to parse two element list with escaped comma" + //); + // assert!(parse_vec("".to_string()).is_err(), "Should fail when parsing empty string"); + // assert!(parse_vec("[a,b".to_string()).is_err(), "Should fail when parsing unclosed list"); + // assert!(parse_vec("a]".to_string()).is_err(), "Should fail when parsing unopened list"); + //} +} diff --git a/crates/simplexpr/src/error.rs b/crates/simplexpr/src/error.rs new file mode 100644 index 0000000..74e193f --- /dev/null +++ b/crates/simplexpr/src/error.rs @@ -0,0 +1,66 @@ +use crate::{ + ast::Span, + dynval, + parser::lexer::{self, LexicalError}, +}; + +pub type Result = std::result::Result; +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Parse error: {source}")] + ParseError { file_id: usize, source: lalrpop_util::ParseError }, + + #[error("Type error: {0}")] + ConversionError(#[from] dynval::ConversionError), + + #[error("{1}")] + Spanned(Span, Box), + + #[error(transparent)] + Eval(#[from] crate::eval::EvalError), + + #[error(transparent)] + Other(#[from] Box), +} + +impl Error { + pub fn from_parse_error(file_id: usize, err: lalrpop_util::ParseError) -> Self { + Error::ParseError { file_id, source: err } + } + + pub fn at(self, span: Span) -> Self { + Self::Spanned(span, Box::new(self)) + } + + pub fn get_span(&self) -> Option { + match self { + Self::ParseError { file_id, source } => get_parse_error_span(*file_id, source), + Self::Spanned(span, _) => Some(*span), + Self::Eval(err) => err.span(), + Self::ConversionError(err) => err.span(), + _ => None, + } + } +} + +fn get_parse_error_span( + file_id: usize, + err: &lalrpop_util::ParseError, +) -> Option { + match err { + lalrpop_util::ParseError::InvalidToken { location } => Some(Span(*location, *location, file_id)), + lalrpop_util::ParseError::UnrecognizedEOF { location, expected: _ } => Some(Span(*location, *location, file_id)), + lalrpop_util::ParseError::UnrecognizedToken { token, expected: _ } => Some(Span(token.0, token.2, file_id)), + lalrpop_util::ParseError::ExtraToken { token } => Some(Span(token.0, token.2, file_id)), + lalrpop_util::ParseError::User { error: LexicalError(l, r) } => Some(Span(*l, *r, file_id)), + } +} + +#[macro_export] +macro_rules! spanned { + ($err:ty, $span:expr, $block:expr) => {{ + let span = $span; + let result: Result<_, $err> = try { $block }; + result.at(span) + }}; +} diff --git a/crates/simplexpr/src/eval.rs b/crates/simplexpr/src/eval.rs new file mode 100644 index 0000000..3c70583 --- /dev/null +++ b/crates/simplexpr/src/eval.rs @@ -0,0 +1,223 @@ +use itertools::Itertools; + +use crate::{ + ast::{BinOp, SimplExpr, Span, UnaryOp}, + dynval::{ConversionError, DynVal}, +}; +use std::collections::HashMap; + +#[derive(Debug, thiserror::Error)] +pub enum EvalError { + #[error("Tried to reference variable `{0}`, but we cannot access variables here")] + NoVariablesAllowed(String), + + #[error("Invalid regex: {0}")] + InvalidRegex(#[from] regex::Error), + + #[error("got unresolved variable `{0}`")] + UnresolvedVariable(VarName), + + #[error("Type error: {0}")] + ConversionError(#[from] ConversionError), + + #[error("Incorrect number of arguments given to function: {0}")] + WrongArgCount(String), + + #[error("Unknown function {0}")] + UnknownFunction(String), + + #[error("Unknown variable {0}")] + UnknownVariable(String), + + #[error("Unable to index into value {0}")] + CannotIndex(String), + + #[error("{1}")] + Spanned(Span, Box), +} + +impl EvalError { + pub fn span(&self) -> Option { + match self { + EvalError::Spanned(span, _) => Some(*span), + EvalError::ConversionError(err) => err.span(), + _ => None, + } + } + + pub fn at(self, span: Span) -> Self { + Self::Spanned(span, Box::new(self)) + } +} + +type VarName = String; + +pub trait FunctionSource { + type Err; + fn run_fn(&self, name: &str, args: &[DynVal]) -> Result; +} + +impl SimplExpr { + pub fn map_terminals_into(self, f: impl Fn(Self) -> Self) -> Self { + use SimplExpr::*; + match self { + BinOp(span, box a, op, box b) => BinOp(span, box f(a), op, box f(b)), + UnaryOp(span, op, box a) => UnaryOp(span, op, box f(a)), + IfElse(span, box a, box b, box c) => IfElse(span, box f(a), box f(b), box f(c)), + JsonAccess(span, box a, box b) => JsonAccess(span, box f(a), box f(b)), + FunctionCall(span, name, args) => FunctionCall(span, name, args.into_iter().map(f).collect()), + other => f(other), + } + } + + /// resolve variable references in the expression. Fails if a variable cannot be resolved. + pub fn resolve_refs(self, variables: &HashMap) -> Result { + use SimplExpr::*; + match self { + // Literal(x) => Ok(Literal(AttrValue::from_primitive(x.resolve_fully(&variables)?))), + Literal(span, x) => Ok(Literal(span, x)), + BinOp(span, box a, op, box b) => Ok(BinOp(span, box a.resolve_refs(variables)?, op, box b.resolve_refs(variables)?)), + UnaryOp(span, op, box x) => Ok(UnaryOp(span, op, box x.resolve_refs(variables)?)), + IfElse(span, box a, box b, box c) => { + Ok(IfElse(span, box a.resolve_refs(variables)?, box b.resolve_refs(variables)?, box c.resolve_refs(variables)?)) + } + JsonAccess(span, box a, box b) => { + Ok(JsonAccess(span, box a.resolve_refs(variables)?, box b.resolve_refs(variables)?)) + } + FunctionCall(span, function_name, args) => Ok(FunctionCall( + span, + function_name, + args.into_iter().map(|a| a.resolve_refs(variables)).collect::>()?, + )), + VarRef(span, ref name) => match variables.get(name) { + Some(value) => Ok(Literal(span, value.clone())), + None => Err(EvalError::UnknownVariable(name.to_string()).at(span)), + }, + } + } + + pub fn var_refs(&self) -> Vec<&String> { + use SimplExpr::*; + match self { + Literal(..) => Vec::new(), + VarRef(_, name) => vec![name], + BinOp(_, box a, _, box b) | JsonAccess(_, box a, box b) => { + let mut refs = a.var_refs(); + refs.append(&mut b.var_refs()); + refs + } + UnaryOp(_, _, box x) => x.var_refs(), + IfElse(_, box a, box b, box c) => { + let mut refs = a.var_refs(); + refs.append(&mut b.var_refs()); + refs.append(&mut c.var_refs()); + refs + } + FunctionCall(_, _, args) => args.iter().flat_map(|a| a.var_refs()).collect_vec(), + } + } + + pub fn eval_no_vars(&self) -> Result { + match self.eval(&HashMap::new()) { + Ok(x) => Ok(x), + Err(EvalError::UnknownVariable(name)) => Err(EvalError::NoVariablesAllowed(name)), + Err(x) => Err(x), + } + } + + pub fn eval(&self, values: &HashMap) -> Result { + let span = self.span(); + let value = match self { + SimplExpr::Literal(_, x) => Ok(x.clone()), + SimplExpr::VarRef(span, ref name) => { + Ok(values.get(name).cloned().ok_or_else(|| EvalError::UnresolvedVariable(name.to_string()).at(*span))?.at(*span)) + } + SimplExpr::BinOp(_, a, op, b) => { + let a = a.eval(values)?; + let b = b.eval(values)?; + Ok(match op { + BinOp::Equals => DynVal::from(a == b), + BinOp::NotEquals => DynVal::from(a != b), + BinOp::And => DynVal::from(a.as_bool()? && b.as_bool()?), + BinOp::Or => DynVal::from(a.as_bool()? || b.as_bool()?), + BinOp::Plus => match a.as_f64() { + Ok(num) => DynVal::from(num + b.as_f64()?), + Err(_) => DynVal::from(format!("{}{}", a.as_string()?, b.as_string()?)), + }, + BinOp::Minus => DynVal::from(a.as_f64()? - b.as_f64()?), + BinOp::Times => DynVal::from(a.as_f64()? * b.as_f64()?), + BinOp::Div => DynVal::from(a.as_f64()? / b.as_f64()?), + BinOp::Mod => DynVal::from(a.as_f64()? % b.as_f64()?), + BinOp::GT => DynVal::from(a.as_f64()? > b.as_f64()?), + BinOp::LT => DynVal::from(a.as_f64()? < b.as_f64()?), + #[allow(clippy::useless_conversion)] + BinOp::Elvis => DynVal::from(if a.0.is_empty() { b } else { a }), + BinOp::RegexMatch => { + let regex = regex::Regex::new(&b.as_string()?)?; + DynVal::from(regex.is_match(&a.as_string()?)) + } + }) + } + SimplExpr::UnaryOp(_, op, a) => { + let a = a.eval(values)?; + Ok(match op { + UnaryOp::Not => DynVal::from(!a.as_bool()?), + }) + } + SimplExpr::IfElse(_, cond, yes, no) => { + if cond.eval(values)?.as_bool()? { + yes.eval(values) + } else { + no.eval(values) + } + } + SimplExpr::JsonAccess(span, val, index) => { + let val = val.eval(values)?; + let index = index.eval(values)?; + match val.as_json_value()? { + serde_json::Value::Array(val) => { + let index = index.as_i32()?; + let indexed_value = val.get(index as usize).unwrap_or(&serde_json::Value::Null); + Ok(DynVal::from(indexed_value)) + } + serde_json::Value::Object(val) => { + let indexed_value = val + .get(&index.as_string()?) + .or_else(|| val.get(&index.as_i32().ok()?.to_string())) + .unwrap_or(&serde_json::Value::Null); + Ok(DynVal::from(indexed_value)) + } + _ => Err(EvalError::CannotIndex(format!("{}", val)).at(*span)), + } + } + SimplExpr::FunctionCall(span, function_name, args) => { + let args = args.into_iter().map(|a| a.eval(values)).collect::>()?; + call_expr_function(&function_name, args).map_err(|e| e.at(*span)) + } + }; + Ok(value?.at(span)) + } +} + +fn call_expr_function(name: &str, args: Vec) -> Result { + match name { + "round" => match args.as_slice() { + [num, digits] => { + let num = num.as_f64()?; + let digits = digits.as_i32()?; + Ok(DynVal::from(format!("{:.1$}", num, digits as usize))) + } + _ => Err(EvalError::WrongArgCount(name.to_string())), + }, + "replace" => match args.as_slice() { + [string, pattern, replacement] => { + let string = string.as_string()?; + let pattern = regex::Regex::new(&pattern.as_string()?)?; + let replacement = replacement.as_string()?; + Ok(DynVal::from(pattern.replace_all(&string, replacement.replace("$", "$$").replace("\\", "$")).into_owned())) + } + _ => Err(EvalError::WrongArgCount(name.to_string())), + }, + _ => Err(EvalError::UnknownFunction(name.to_string())), + } +} diff --git a/crates/simplexpr/src/lib.rs b/crates/simplexpr/src/lib.rs new file mode 100644 index 0000000..74df615 --- /dev/null +++ b/crates/simplexpr/src/lib.rs @@ -0,0 +1,21 @@ +#![feature(box_patterns)] +#![feature(box_syntax)] +#![feature(try_blocks)] +pub mod ast; +pub mod dynval; +pub mod error; +pub mod eval; +pub mod parser; + +pub use ast::{SimplExpr, Span}; + +use lalrpop_util::lalrpop_mod; + +lalrpop_mod!( + #[allow(clippy::all)] + pub simplexpr_parser +); + +pub fn parse_string(file_id: usize, s: &str) -> Result { + parser::parse_string(file_id, s) +} diff --git a/crates/simplexpr/src/parser/lalrpop_helpers.rs b/crates/simplexpr/src/parser/lalrpop_helpers.rs new file mode 100644 index 0000000..f642a30 --- /dev/null +++ b/crates/simplexpr/src/parser/lalrpop_helpers.rs @@ -0,0 +1,3 @@ +pub fn b(x: T) -> Box { + Box::new(x) +} diff --git a/crates/simplexpr/src/parser/lexer.rs b/crates/simplexpr/src/parser/lexer.rs new file mode 100644 index 0000000..e3c2447 --- /dev/null +++ b/crates/simplexpr/src/parser/lexer.rs @@ -0,0 +1,78 @@ +use logos::Logos; + +#[rustfmt::skip] +#[derive(Logos, Debug, PartialEq, Eq, Clone, strum::Display, strum::EnumString)] +pub enum Token { + #[strum(serialize = "+") ] #[token("+") ] Plus, + #[strum(serialize = "-") ] #[token("-") ] Minus, + #[strum(serialize = "*") ] #[token("*") ] Times, + #[strum(serialize = "/") ] #[token("/") ] Div, + #[strum(serialize = "%") ] #[token("%") ] Mod, + #[strum(serialize = "==")] #[token("==")] Equals, + #[strum(serialize = "!=")] #[token("!=")] NotEquals, + #[strum(serialize = "&&")] #[token("&&")] And, + #[strum(serialize = "||")] #[token("||")] Or, + #[strum(serialize = ">") ] #[token(">") ] GT, + #[strum(serialize = "<") ] #[token("<") ] LT, + #[strum(serialize = "?:")] #[token("?:")] Elvis, + #[strum(serialize = "=~")] #[token("=~")] RegexMatch, + + #[strum(serialize = "!") ] #[token("!") ] Not, + + #[strum(serialize = ",") ] #[token(",") ] Comma, + #[strum(serialize = "?") ] #[token("?") ] Question, + #[strum(serialize = ":") ] #[token(":") ] Colon, + #[strum(serialize = "(") ] #[token("(") ] LPren, + #[strum(serialize = ")") ] #[token(")") ] RPren, + #[strum(serialize = "[") ] #[token("[") ] LBrack, + #[strum(serialize = "]") ] #[token("]") ] RBrack, + #[strum(serialize = ".") ] #[token(".") ] Dot, + #[strum(serialize = "true") ] #[token("true") ] True, + #[strum(serialize = "false")] #[token("false")] False, + + #[regex(r"[a-zA-Z_-]+", |x| x.slice().to_string())] + Ident(String), + #[regex(r"[+-]?(?:[0-9]+[.])?[0-9]+", |x| x.slice().to_string())] + NumLit(String), + #[regex(r#""(?:[^"\\]|\\.)*""#, |x| x.slice().to_string())] + StrLit(String), + + + #[error] + #[regex(r"[ \t\n\f]+", logos::skip)] + Error, +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct LexicalError(pub usize, pub usize); + +impl std::fmt::Display for LexicalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Lexical error at {}..{}", self.0, self.1) + } +} + +pub type SpannedResult = Result<(Loc, Tok, Loc), Error>; + +pub struct Lexer<'input> { + lexer: logos::SpannedIter<'input, Token>, +} + +impl<'input> Lexer<'input> { + pub fn new(text: &'input str) -> Self { + Lexer { lexer: logos::Lexer::new(text).spanned() } + } +} + +impl<'input> Iterator for Lexer<'input> { + type Item = SpannedResult; + + fn next(&mut self) -> Option { + let (token, range) = self.lexer.next()?; + if token == Token::Error { + Some(Err(LexicalError(range.start, range.end))) + } else { + Some(Ok((range.start, token, range.end))) + } + } +} diff --git a/crates/simplexpr/src/parser/mod.rs b/crates/simplexpr/src/parser/mod.rs new file mode 100644 index 0000000..f833b61 --- /dev/null +++ b/crates/simplexpr/src/parser/mod.rs @@ -0,0 +1,47 @@ +pub mod lalrpop_helpers; +pub mod lexer; + +use crate::{ + ast::SimplExpr, + error::{Error, Result}, +}; + +pub fn parse_string(file_id: usize, s: &str) -> Result { + let lexer = lexer::Lexer::new(s); + let parser = crate::simplexpr_parser::ExprParser::new(); + parser.parse(file_id, lexer).map_err(|e| Error::from_parse_error(file_id, e)) +} + +#[cfg(test)] +mod tests { + macro_rules! test_parser { + ($($text:literal),* $(,)?) => {{ + let p = crate::simplexpr_parser::ExprParser::new(); + use crate::parser::lexer::Lexer; + ::insta::with_settings!({sort_maps => true}, { + $( + ::insta::assert_debug_snapshot!(p.parse(0, Lexer::new($text))); + )* + }); + }} + } + + #[test] + fn test() { + test_parser!( + "1", + "2 + 5", + "2 * 5 + 1 * 1 + 3", + "(1 + 2) * 2", + "1 + true ? 2 : 5", + "1 + true ? 2 : 5 + 2", + "1 + (true ? 2 : 5) + 2", + "foo(1, 2)", + "! false || ! true", + "\"foo\" + 12.4", + "hi[\"ho\"]", + "foo.bar.baz", + "foo.bar[2 + 2] * asdf[foo.bar]", + ); + } +} diff --git a/crates/simplexpr/src/simplexpr_parser.lalrpop b/crates/simplexpr/src/simplexpr_parser.lalrpop new file mode 100644 index 0000000..f351087 --- /dev/null +++ b/crates/simplexpr/src/simplexpr_parser.lalrpop @@ -0,0 +1,121 @@ +use crate::ast::{SimplExpr::{self, *}, Span, BinOp::*, UnaryOp::*}; +use crate::parser::lexer::{Token, LexicalError}; +use crate::parser::lalrpop_helpers::*; +use lalrpop_util::ParseError; + + +grammar(fid: usize); + +extern { + type Location = usize; + type Error = LexicalError; + + enum Token { + "+" => Token::Plus, + "-" => Token::Minus, + "*" => Token::Times, + "/" => Token::Div, + "%" => Token::Mod, + "==" => Token::Equals, + "!=" => Token::NotEquals, + "&&" => Token::And, + "||" => Token::Or, + ">" => Token::GT, + "<" => Token::LT, + "?:" => Token::Elvis, + "=~" => Token::RegexMatch, + + "!" => Token::Not, + + "," => Token::Comma, + "?" => Token::Question, + ":" => Token::Colon, + "(" => Token::LPren, + ")" => Token::RPren, + "[" => Token::LBrack, + "]" => Token::RBrack, + "." => Token::Dot, + + "true" => Token::True, + "false" => Token::False, + + "identifier" => Token::Ident(), + "number" => Token::NumLit(), + "string" => Token::StrLit(), + + "lexer_error" => Token::Error, + + } +} + +Comma: Vec = { + ",")*> => match e { + None => v, + Some(e) => { + v.push(e); + v + } + } +}; + +pub Expr: SimplExpr = { + + #[precedence(level="0")] + "lexer_error" =>? { + Err(ParseError::User { error: LexicalError(l, r) }) + }, + + , + => VarRef(Span(l, r, fid), ident.to_string()), + "(" ")", + + #[precedence(level="1")] #[assoc(side="right")] + "(" > ")" => FunctionCall(Span(l, r, fid), ident, args), + "[" "]" => JsonAccess(Span(l, r, fid), b(value), b(index)), + + "." => { + JsonAccess(Span(l, r, fid), b(value), b(Literal(Span(lit_l, r, fid), index.into()))) + }, + + #[precedence(level="2")] #[assoc(side="right")] + "!" => UnaryOp(Span(l, r, fid), Not, b(e)), + + #[precedence(level="3")] #[assoc(side="left")] + "*" => BinOp(Span(l, r, fid), b(le), Times, b(re)), + "/" => BinOp(Span(l, r, fid), b(le), Div, b(re)), + "%" => BinOp(Span(l, r, fid), b(le), Mod, b(re)), + + #[precedence(level="4")] #[assoc(side="left")] + "+" => BinOp(Span(l, r, fid), b(le), Plus, b(re)), + "-" => BinOp(Span(l, r, fid), b(le), Minus, b(re)), + + #[precedence(level="5")] #[assoc(side="left")] + "==" => BinOp(Span(l, r, fid), b(le), Equals, b(re)), + "!=" => BinOp(Span(l, r, fid), b(le), NotEquals, b(re)), + "<" => BinOp(Span(l, r, fid), b(le), GT, b(re)), + ">" => BinOp(Span(l, r, fid), b(le), LT, b(re)), + "=~" => BinOp(Span(l, r, fid), b(le), RegexMatch, b(re)), + + #[precedence(level="6")] #[assoc(side="left")] + "&&" => BinOp(Span(l, r, fid), b(le), And, b(re)), + "||" => BinOp(Span(l, r, fid), b(le), Or, b(re)), + "?:" => BinOp(Span(l, r, fid), b(le), Elvis, b(re)), + + #[precedence(level="7")] #[assoc(side="right")] + "?" ":" => { + IfElse(Span(l, r, fid), b(cond), b(then), b(els)) + }, +}; + +ExprReset = ; + +Literal: SimplExpr = { + => SimplExpr::literal(Span(l, r, fid), x), + => SimplExpr::literal(Span(l, r, fid), x), + "true" => SimplExpr::literal(Span(l, r, fid), "true".into()), + "false" => SimplExpr::literal(Span(l, r, fid), "false".into()), +} + +StrLit: String = { + => x[1..x.len() - 1].to_owned(), +}; diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-10.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-10.snap new file mode 100644 index 0000000..1fa28f0 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-10.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"\\\"foo\\\" + 12.4\"))" + +--- +Ok( + ("foo" + "12.4"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-11.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-11.snap new file mode 100644 index 0000000..44b65b7 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-11.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"hi[\\\"ho\\\"]\"))" + +--- +Ok( + hi["ho"], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-12.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-12.snap new file mode 100644 index 0000000..e19354b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-12.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo.bar.baz\"))" + +--- +Ok( + foo["bar"]["baz"], +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-13.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-13.snap new file mode 100644 index 0000000..7494deb --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-13.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo.bar[2 + 2] * asdf[foo.bar]\"))" + +--- +Ok( + (foo["bar"][("2" + "2")] * asdf[foo["bar"]]), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-14.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-14.snap new file mode 100644 index 0000000..692ac35 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-14.snap @@ -0,0 +1,30 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1 + (true ? 2 : 5) + 2\"))" + +--- +Ok( + BinOp( + BinOp( + Literal( + "1", + ), + Plus, + IfElse( + Literal( + "true", + ), + Literal( + "2", + ), + Literal( + "5", + ), + ), + ), + Plus, + Literal( + "2", + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-15.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-15.snap new file mode 100644 index 0000000..16b8e8a --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-15.snap @@ -0,0 +1,13 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"foo(1, 2)\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x) |\n Token::StrLit(x) =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::>()" + +--- +[ + "foo", + "LPren", + "1", + "Comma", + "2", + "RPren", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-16.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-16.snap new file mode 100644 index 0000000..e128f15 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-16.snap @@ -0,0 +1,18 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo(1, 2)\"))" + +--- +Ok( + FunctionCall( + "foo", + [ + Literal( + "1", + ), + Literal( + "2", + ), + ], + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-17.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-17.snap new file mode 100644 index 0000000..ae5f678 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-17.snap @@ -0,0 +1,12 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"! false || ! true\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x)\n |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\",\n x),\n x =>\n format!(\"{}\",\n x),\n }).collect::>()" + +--- +[ + "!", + "False", + "||", + "!", + "True", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-18.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-18.snap new file mode 100644 index 0000000..9ffd512 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-18.snap @@ -0,0 +1,22 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"! false || ! true\"))" + +--- +Ok( + BinOp( + UnaryOp( + Not, + Literal( + "false", + ), + ), + Or, + UnaryOp( + Not, + Literal( + "true", + ), + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-19.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-19.snap new file mode 100644 index 0000000..8d6b02b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-19.snap @@ -0,0 +1,10 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"\\\"foo\\\" + 12.4\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x)\n |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\",\n x),\n x =>\n format!(\"{}\",\n x),\n }).collect::>()" + +--- +[ + "\"foo\"", + "+", + "12.4", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-2.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-2.snap new file mode 100644 index 0000000..3b4e5e4 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-2.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"2 + 5\"))" + +--- +Ok( + ("2" + "5"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-20.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-20.snap new file mode 100644 index 0000000..9d42e5a --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-20.snap @@ -0,0 +1,16 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"\\\"foo\\\" + 12.4\"))" + +--- +Ok( + BinOp( + Literal( + "foo", + ), + Plus, + Literal( + "12.4", + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-21.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-21.snap new file mode 100644 index 0000000..84a5eb7 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-21.snap @@ -0,0 +1,11 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"hi[\\\"ho\\\"]\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x) |\n Token::StrLit(x)\n =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::>()" + +--- +[ + "hi", + "LBrack", + "\"ho\"", + "RBrack", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-22.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-22.snap new file mode 100644 index 0000000..958cec3 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-22.snap @@ -0,0 +1,15 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"hi[\\\"ho\\\"]\"))" + +--- +Ok( + JsonAccess( + VarRef( + "hi", + ), + Literal( + "ho", + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-23.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-23.snap new file mode 100644 index 0000000..6e91f79 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-23.snap @@ -0,0 +1,12 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"foo.bar.baz\").filter_map(|x|\n x.ok()).map(|(_, x, _)|\n match x {\n Token::Ident(x) |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\", x),\n x =>\n format!(\"{}\", x),\n }).collect::>()" + +--- +[ + "foo", + "Dot", + "bar", + "Dot", + "baz", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-24.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-24.snap new file mode 100644 index 0000000..be97009 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-24.snap @@ -0,0 +1,20 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo.bar.baz\"))" + +--- +Ok( + JsonAccess( + JsonAccess( + VarRef( + "foo", + ), + Literal( + "bar", + ), + ), + Literal( + "baz", + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-25.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-25.snap new file mode 100644 index 0000000..3a21b7b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-25.snap @@ -0,0 +1,22 @@ +--- +source: src/lib.rs +expression: "Lexer::new(\"foo.bar[2 + 2] * asdf[foo.bar]\").filter_map(|x|\n x.ok()).map(|(_,\n x,\n _)|\n match x\n {\n Token::Ident(x)\n |\n Token::NumLit(x)\n |\n Token::StrLit(x)\n =>\n format!(\"{}\",\n x),\n x\n =>\n format!(\"{}\",\n x),\n }).collect::>()" + +--- +[ + "foo", + "Dot", + "bar", + "LBrack", + "2", + "+", + "2", + "RBrack", + "*", + "asdf", + "LBrack", + "foo", + "Dot", + "bar", + "RBrack", +] diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-26.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-26.snap new file mode 100644 index 0000000..bdabe4e --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-26.snap @@ -0,0 +1,42 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo.bar[2 + 2] * asdf[foo.bar]\"))" + +--- +Ok( + BinOp( + JsonAccess( + JsonAccess( + VarRef( + "foo", + ), + Literal( + "bar", + ), + ), + BinOp( + Literal( + "2", + ), + Plus, + Literal( + "2", + ), + ), + ), + Times, + JsonAccess( + VarRef( + "asdf", + ), + JsonAccess( + VarRef( + "foo", + ), + Literal( + "bar", + ), + ), + ), + ), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-3.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-3.snap new file mode 100644 index 0000000..7a11e2b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-3.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"2 * 5 + 1 * 1 + 3\"))" + +--- +Ok( + ((("2" * "5") + ("1" * "1")) + "3"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-4.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-4.snap new file mode 100644 index 0000000..9025ed3 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-4.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"(1 + 2) * 2\"))" + +--- +Ok( + (("1" + "2") * "2"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-5.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-5.snap new file mode 100644 index 0000000..683c97b --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-5.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1 + true ? 2 : 5\"))" + +--- +Ok( + (if ("1" + "true") then "2" else "5"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-6.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-6.snap new file mode 100644 index 0000000..ca3c9ba --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-6.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1 + true ? 2 : 5 + 2\"))" + +--- +Ok( + (if ("1" + "true") then "2" else ("5" + "2")), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-7.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-7.snap new file mode 100644 index 0000000..bd2587c --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-7.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1 + (true ? 2 : 5) + 2\"))" + +--- +Ok( + (("1" + (if "true" then "2" else "5")) + "2"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-8.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-8.snap new file mode 100644 index 0000000..cbd02d1 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-8.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"foo(1, 2)\"))" + +--- +Ok( + foo("1", "2"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test-9.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test-9.snap new file mode 100644 index 0000000..eba646a --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test-9.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"! false || ! true\"))" + +--- +Ok( + (!"false" || !"true"), +) diff --git a/crates/simplexpr/src/snapshots/simplexpr__tests__test.snap b/crates/simplexpr/src/snapshots/simplexpr__tests__test.snap new file mode 100644 index 0000000..6c1e712 --- /dev/null +++ b/crates/simplexpr/src/snapshots/simplexpr__tests__test.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: "p.parse(Lexer::new(\"1\"))" + +--- +Ok( + "1", +)