diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..832f829 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: cargo test --release + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: cargo fmt --all -- --check + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: cargo clippy -- -D warnings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab61aba --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +test.is diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9f5196b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,905 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aiscript" +version = "0.1.0" +dependencies = [ + "chrono", + "futures", + "indexmap", + "peg", + "percent-encoding", + "rand", + "regex", + "rustyline", + "serde", + "serde_json", + "thiserror", + "tokio", + "unicode-segmentation", + "uuid", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cc" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "clipboard-win" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +dependencies = [ + "error-code", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "peg" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "400bcab7d219c38abf8bd7cc2054eb9bbbd4312d66f6a5557d572a203f646f61" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e61cce859b76d19090f62da50a9fe92bab7c2a5f09e183763559a2ac392c90" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bae92c60fa2398ce4678b98b2c4b5a7c61099961ca1fa305aec04a9ad28922" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustyline" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "num_cpus", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b2573fa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "aiscript" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4" +futures = "0.3" +indexmap = "2.2" +peg = "0.8" +percent-encoding = "2.3" +rand = "0.8" +regex = "1.10" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["preserve_order"] } +thiserror = "1.0" +tokio = { version = "1", features = ["rt", "time"] } +unicode-segmentation = "1.11" +uuid = { version = "1.8", features = ["v4"] } + +[dev-dependencies] +rustyline = "14.0" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..ed7d70a --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2024 poppingmoon + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..156902a --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# aiscript-rs + +[![CI](https://github.com/poppingmoon/aiscript-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/poppingmoon/aiscript-rs/actions/workflows/ci.yml) + +Rust implementation of [AiScript](https://github.com/aiscript-dev/aiscript) (Experimental) + +## Example + +```rust +use aiscript::{Interpreter, Parser}; +use futures::FutureExt; + +let script = Parser::default().parse("<: 'Hello, world!'")?; +let interpreter = Interpreter::new( + [], + None:: _>, + Some(|v| { + println!("{v}"); + async move {}.boxed() + }), + None:: _>, + None, +); +interpreter.exec(script).await?; +``` diff --git a/examples/console.rs b/examples/console.rs new file mode 100644 index 0000000..918b0ae --- /dev/null +++ b/examples/console.rs @@ -0,0 +1,86 @@ +use std::io::{prelude::*, stdin, stdout}; + +use aiscript::{ + values::{Value, V}, + Interpreter, Parser, +}; +use futures::FutureExt; +use rustyline::error::ReadlineError; +use rustyline::{DefaultEditor, Result}; + +#[tokio::main] +async fn main() -> Result<()> { + let mut rl = DefaultEditor::new()?; + let parser = Parser::default(); + let aiscript = Interpreter::new( + [], + Some(|q| { + print!("{q}"); + stdout().flush().unwrap(); + let mut buf = String::new(); + stdin().read_line(&mut buf).unwrap(); + async move { buf }.boxed() + }), + Some(|v: Value| { + println!("{}", v.value.repr_value()); + async move {}.boxed() + }), + Some(|e| { + eprintln!("Error: {e}"); + async move {}.boxed() + }), + None, + ); + let mut input = String::new(); + println!("Welcome to AiScript!"); + println!("https://github.com/aiscript-dev/aiscript"); + println!(); + println!("Type 'exit' to end this session."); + loop { + let readline = rl.readline(if input.is_empty() { "> " } else { ". " }); + match readline { + Ok(line) => { + if !input.is_empty() { + input += "\n"; + } + input += &line; + if input == "exit" { + println!("Bye."); + break; + } + let script = parser.parse(&input); + match script { + Ok(script) => { + rl.add_history_entry(&input)?; + input.clear(); + let result = aiscript.exec(script).await.unwrap(); + match result { + Some(Value { value: V::Null, .. }) | None => (), + Some(Value { value, .. }) => println!("{}", value.repr_value()), + } + } + Err(err) => { + if line.trim().is_empty() { + rl.add_history_entry(&input)?; + input.clear(); + eprintln!("Error: {err:?}"); + } + } + } + } + Err(ReadlineError::Interrupted) => { + input.clear(); + println!("Interrupted."); + } + Err(ReadlineError::Eof) => { + println!("Bye."); + break; + } + Err(err) => { + eprintln!("Error: {err:?}"); + break; + } + } + } + Ok(()) +} diff --git a/examples/parse.rs b/examples/parse.rs new file mode 100644 index 0000000..87930c6 --- /dev/null +++ b/examples/parse.rs @@ -0,0 +1,12 @@ +use std::{fs::File, io::prelude::*}; + +use aiscript::Parser; + +#[tokio::main] +async fn main() { + let mut file = File::open("test.is").unwrap(); + let mut s = String::new(); + file.read_to_string(&mut s).unwrap(); + let script = Parser::default().parse(&s).unwrap(); + println!("{script:?}"); +} diff --git a/examples/run.rs b/examples/run.rs new file mode 100644 index 0000000..b571e32 --- /dev/null +++ b/examples/run.rs @@ -0,0 +1,44 @@ +use std::{ + fs::File, + io::{prelude::*, stdin, stdout}, +}; + +use aiscript::{values::Value, Interpreter, Parser}; +use futures::FutureExt; + +#[tokio::main] +async fn main() { + let mut file = File::open("test.is").unwrap(); + let mut s = String::new(); + file.read_to_string(&mut s).unwrap(); + let script = Parser::default().parse(&s).unwrap(); + let aiscript = Interpreter::new( + [], + Some(|q| { + print!("{q}"); + stdout().flush().unwrap(); + let mut buf = String::new(); + stdin().read_line(&mut buf).unwrap(); + async move { buf }.boxed() + }), + Some(|v: Value| { + println!("{}", v.value.repr_value()); + async move {}.boxed() + }), + Some(|e| { + eprintln!("{e}"); + async move {}.boxed() + }), + None, + ); + println!( + "{}", + aiscript + .exec(script) + .await + .unwrap() + .unwrap_or_default() + .value + .repr_value() + ); +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..6c20677 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1 @@ +pub const AISCRIPT_VERSION: &str = "0.19.0"; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..4676dd8 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,33 @@ +use peg::{error::ParseError, str::LineCol}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum AiScriptError { + #[error("Internal: {0}")] + Internal(String), + #[error("Syntax: {0}")] + Syntax(#[from] AiScriptSyntaxError), + // Type, + #[error(transparent)] + Runtime(#[from] AiScriptRuntimeError), +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum AiScriptSyntaxError { + #[error("Parsing error. (Line {}:{})", .0.location.line, .0.location.column)] + Parse(#[from] ParseError), + #[error("invalid attribute.")] + Attribute, + #[error(r#"Reserved word "{0}" cannot be used as variable name."#)] + ReservedWord(String), +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum AiScriptRuntimeError { + #[error("Runtime: {0}")] + Runtime(String), + #[error("Runtime: Index out of range. index: {0} max: {1}")] + IndexOutOfRange(f64, isize), + #[error("{0}")] + User(String), +} diff --git a/src/interpreter.rs b/src/interpreter.rs new file mode 100644 index 0000000..125908c --- /dev/null +++ b/src/interpreter.rs @@ -0,0 +1,770 @@ +//! AiScript interpreter + +use std::{ + collections::HashMap, + iter::{repeat, zip}, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, Mutex, + }, + time::Duration, +}; + +use futures::{ + future::{try_join_all, BoxFuture}, + Future, FutureExt, +}; +use indexmap::IndexMap; +use value::VObj; + +use crate::{ + error::{AiScriptError, AiScriptRuntimeError}, + node as ast, +}; + +use self::{ + lib::std::std, + primitive_props::get_prim_prop, + scope::Scope, + util::expect_any, + value::{unwrap_ret, Attr, VFn, Value, V}, + variable::Variable, +}; + +mod lib; +mod primitive_props; +pub mod scope; +pub mod util; +pub mod value; +mod variable; + +const IRQ_RATE: usize = 300; +const IRQ_AT: usize = IRQ_RATE - 1; + +#[derive(Clone, Default)] +pub struct Interpreter { + pub step_count: Arc, + stop: Arc, + pub scope: Scope, + abort_handlers: Arc>>>, + err: Option BoxFuture<'static, ()>) + Sync + Send + 'static>>, + max_step: Option, +} + +impl std::fmt::Debug for Interpreter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Interpreter") + .field("step_count", &self.step_count) + .field("stop", &self.stop) + .field("scope", &self.scope) + .field("max_step", &self.max_step) + .finish() + } +} + +impl Interpreter { + pub fn new( + consts: impl IntoIterator, + in_: Option BoxFuture<'static, String> + Sync + Send + Clone + 'static>, + out: Option BoxFuture<'static, ()> + Sync + Send + Clone + 'static>, + err: Option BoxFuture<'static, ()> + Sync + Send + 'static>, + max_step: Option, + ) -> Self { + let io = [ + ( + "print".to_string(), + Value::fn_native(move |args, _| { + let out = out.clone(); + async move { + let mut args = args.into_iter(); + let v = expect_any(args.next())?; + if let Some(out) = out { + out(v).await; + } + Ok(Value::null()) + } + .boxed() + }), + ), + ( + "readline".to_string(), + Value::fn_native(move |args, _| { + let in_ = in_.clone(); + async move { + let mut args = args.into_iter(); + let q = String::try_from(args.next().unwrap_or_default())?; + if let Some(in_) = in_ { + let a = in_(q).await; + Ok(Value::str(a)) + } else { + Ok(Value::null()) + } + } + .boxed() + }), + ), + ]; + let mut states = Vec::from_iter(consts); + states.extend(std()); + states.extend(io); + let states = states + .into_iter() + .map(|(k, v)| (k, Variable::Const(v))) + .collect(); + Interpreter { + step_count: Arc::new(AtomicUsize::new(0)), + stop: Arc::new(AtomicBool::new(false)), + scope: Scope::new(states, None), + abort_handlers: Arc::new(Mutex::new(tokio::task::JoinSet::new())), + err: match err { + Some(err) => Some(Arc::new(err)), + None => None, + }, + max_step, + } + } + + pub async fn exec(&self, script: Vec) -> Result, AiScriptError> { + self.stop.store(false, Ordering::SeqCst); + let script = self.collect_ns(script, self.scope.clone()).await?; + let result = self.run(script, &self.scope).await; + self.handle_error(result).await + } + + /// Executes AiScript Function. + /// + /// When it fails, + /// 1. If error callback is registered via constructor, [`Self::abort`] is called and the callback executed, then returns ERROR('func_failed'). + /// 2. Otherwise, just returns an error. + pub async fn exec_fn( + &self, + fn_: VFn, + args: impl IntoIterator, + ) -> Result { + let result = self.fn_(fn_, args).await; + let result = self.handle_error(result).await?; + Ok(result.unwrap_or_else(|| Value::error("func_failed", None))) + } + + /// Executes AiScript Function. + /// + /// Almost same as [`Self::exec_fn`] but when error occurs this always returns it and never calls callback. + pub async fn exec_fn_simple( + &self, + fn_: VFn, + args: impl IntoIterator, + ) -> Result { + self.fn_(fn_, args).await + } + + pub fn collect_metadata(script: Vec) -> IndexMap, Option> { + fn node_to_value(node: ast::Expression) -> Option { + match node { + ast::Expression::Arr(ast::Arr { value, .. }) => Some(Value::arr({ + let mut vec = Vec::new(); + for node in value { + if let Some(value) = node_to_value(node) { + vec.push(value); + } + } + vec + })), + ast::Expression::Bool(ast::Bool { value, .. }) => Some(Value::bool(value)), + ast::Expression::Null(_) => Some(Value::null()), + ast::Expression::Num(ast::Num { value, .. }) => Some(Value::num(value)), + ast::Expression::Obj(ast::Obj { value, .. }) => Some(Value::obj({ + let mut obj = IndexMap::new(); + for (k, v) in value.into_iter() { + if let Some(value) = node_to_value(v) { + obj.insert(k, value); + } + } + obj + })), + ast::Expression::Str(ast::Str { value, .. }) => Some(Value::str(value)), + _ => None, + } + } + + let mut meta = IndexMap::new(); + + for node in script { + if let ast::Node::Meta(ast::Meta { name, value, .. }) = node { + meta.insert(name, node_to_value(value)); + } + } + + meta + } + + async fn handle_error( + &self, + result: Result, + ) -> Result, AiScriptError> { + match result { + Ok(value) => Ok(Some(value)), + Err(e) => { + if let Some(err) = &self.err { + if !self.stop.load(Ordering::SeqCst) { + self.abort(); + err(e).await; + return Ok(None); + } + } + Err(e) + } + } + } + + async fn collect_ns( + &self, + script: impl IntoIterator>, + scope: Scope, + ) -> Result, AiScriptError> { + let mut nodes = Vec::new(); + for node in script { + match node.into() { + ast::Node::Namespace(namespace) => { + self.collect_ns_member(namespace.clone(), scope.clone()) + .await?; + nodes.push(ast::Node::Namespace(namespace)); + } + node => nodes.push(node), + } + } + Ok(nodes) + } + + fn collect_ns_member( + &self, + ns: ast::Namespace, + scope: Scope, + ) -> BoxFuture<'_, Result<(), AiScriptError>> { + async move { + let ns_scope = scope.create_child_namespace_scope(ns.name, HashMap::new(), None); + for node in &ns.members { + if let ast::DefinitionOrNamespace::Namespace(ns) = node { + self.collect_ns_member(ns.clone(), ns_scope.clone()).await?; + } + } + for node in ns.members { + if let ast::DefinitionOrNamespace::Definition(ast::Definition { + name, + expr, + mut_, + .. + }) = node + { + if mut_ { + Err(AiScriptError::Internal( + "Namespaces cannot include mutable variable: {name}".to_string(), + ))?; + } else { + let variable = Variable::Const(self.eval(expr, &ns_scope).await?); + ns_scope.add(name, variable)?; + } + } + } + Ok(()) + } + .boxed() + } + + fn fn_( + &self, + fn_: VFn, + args: impl IntoIterator, + ) -> BoxFuture<'_, Result> { + match fn_ { + VFn::Fn { + args: fn_args, + statements, + scope, + } => { + let args = zip( + fn_args, + args.into_iter() + .chain(repeat(Value::null())) + .map(Variable::Mut), + ) + .collect(); + async move { + self.run(statements, &scope.create_child_scope(args, None)) + .map(|r| r.map(unwrap_ret)) + .await + } + .boxed() + } + VFn::FnNative(fn_) => fn_(args.into_iter().collect(), self), + } + } + + fn eval<'a>( + &'a self, + node: impl Into, + scope: &'a Scope, + ) -> BoxFuture<'a, Result> { + if self.stop.load(Ordering::SeqCst) { + return async move { Ok(Value::null()) }.boxed(); + } + let node = node.into(); + async move { + let step_count = self.step_count.load(Ordering::SeqCst); + if step_count % IRQ_RATE == IRQ_AT { + tokio::time::sleep(Duration::from_millis(5)).await; + } + let step_count = self.step_count.fetch_add(1, Ordering::SeqCst); + if let Some(max_step) = self.max_step { + if step_count > max_step { + Err(AiScriptRuntimeError::Runtime( + "max step exceeded".to_string(), + ))? + } + } + Ok(match node { + ast::Node::Namespace(_) | ast::Node::Meta(_) => Value::null(), + ast::Node::Statement(statement) => match statement { + ast::Statement::Definition(ast::Definition { + name, + expr, + mut_, + attr, + .. + }) => { + let value = self.eval(expr, scope).await?; + let attr = match attr { + Some(attr) => { + let mut attrs = Vec::new(); + for n_attr in attr { + attrs.push(Attr { + name: n_attr.name, + value: self.eval(n_attr.value, scope).await?, + }) + } + Some(attrs) + } + None => None, + }; + scope.add( + name, + if mut_ { + Variable::Mut(Value { attr, ..value }) + } else { + Variable::Const(Value { attr, ..value }) + }, + )?; + Value::null() + } + ast::Statement::Return(ast::Return { expr, .. }) => { + let val = self.eval(expr, scope).await?; + Value::return_(val) + } + ast::Statement::Each(ast::Each { + items, for_, var, .. + }) => { + let items = self.eval(items, scope).await?; + let items = >::try_from(items)?; + for item in items { + let scope = scope.create_child_scope( + HashMap::from_iter([(var.clone(), Variable::Const(item))]), + None, + ); + let v = self.eval(*for_.clone(), &scope).await?; + match v.value { + V::Break => { + break; + } + V::Return(_) => { + return Ok(v); + } + _ => (), + } + } + Value::null() + } + ast::Statement::For(ast::For { + times, + from, + var, + to, + for_, + .. + }) => { + if let Some(times) = times { + let times = self.eval(times, scope).await?; + let times = f64::try_from(times)?; + let mut i = 0.0; + while i < times { + let v = self.eval(*for_.clone(), scope).await?; + match v.value { + V::Break => { + break; + } + V::Return(_) => { + return Ok(v); + } + _ => (), + } + i += 1.0; + } + } else if let (Some(from), Some(to), Some(var)) = (from, to, var) { + let from = self.eval(from, scope).await?; + let to = self.eval(to, scope).await?; + let from = f64::try_from(from)?; + let to = f64::try_from(to)?; + let mut i = from; + while i < from + to { + let scope = scope.create_child_scope( + HashMap::from_iter([( + var.clone(), + Variable::Const(Value::num(i)), + )]), + None, + ); + let v = self.eval(*for_.clone(), &scope).await?; + match v.value { + V::Break => { + break; + } + V::Return(_) => { + return Ok(v); + } + _ => (), + } + i += 1.0; + } + } + Value::null() + } + ast::Statement::Loop(ast::Loop { statements, .. }) => loop { + let v = self + .run( + statements.clone(), + &scope.create_child_scope(HashMap::new(), None), + ) + .await?; + match v.value { + V::Break => { + break Value::null(); + } + V::Return(_) => { + break v; + } + _ => (), + } + }, + ast::Statement::Break(_) => Value::break_(), + ast::Statement::Continue(_) => Value::continue_(), + ast::Statement::Assign(ast::Assign { expr, dest, .. }) => { + let v = self.eval(expr, scope).await?; + self.assign(scope, dest, v).await?; + Value::null() + } + ast::Statement::AddAssign(ast::AddAssign { expr, dest, .. }) => { + let target = self.eval(dest.clone(), scope).await?; + let target = f64::try_from(target)?; + let v = self.eval(expr, scope).await?; + let v = f64::try_from(v)?; + self.assign(scope, dest, Value::num(target + v)).await?; + Value::null() + } + ast::Statement::SubAssign(ast::SubAssign { expr, dest, .. }) => { + let target = self.eval(dest.clone(), scope).await?; + let target = f64::try_from(target)?; + let v = self.eval(expr, scope).await?; + let v = f64::try_from(v)?; + self.assign(scope, dest, Value::num(target - v)).await?; + Value::null() + } + }, + ast::Node::Expression(expression) => match expression { + ast::Expression::If(ast::If { + cond, + then, + elseif, + else_, + .. + }) => { + let cond = self.eval(*cond, scope).await?; + let cond = bool::try_from(cond)?; + if cond { + self.eval(*then, scope).await? + } else { + for ast::Elseif { cond, then } in elseif { + let cond = self.eval(cond, scope).await?; + let cond = bool::try_from(cond)?; + if cond { + return self.eval(then, scope).await; + } + } + if let Some(else_) = else_ { + self.eval(*else_, scope).await? + } else { + Value::null() + } + } + } + ast::Expression::Fn(ast::Fn { args, children, .. }) => Value::fn_( + args.into_iter().map(|arg| arg.name), + children, + scope.clone(), + ), + ast::Expression::Match(ast::Match { + about, qs, default, .. + }) => { + let about = self.eval(*about, scope).await?; + for ast::QA { q, a } in qs { + let q = self.eval(q, scope).await?; + if about == q { + return self.eval(a, scope).await; + } + } + if let Some(default) = default { + self.eval(*default, scope).await? + } else { + Value::null() + } + } + ast::Expression::Block(ast::Block { statements, .. }) => { + self.run(statements, &scope.create_child_scope(HashMap::new(), None)) + .await? + } + ast::Expression::Exists(ast::Exists { identifier, .. }) => { + Value::bool(scope.exists(&identifier.name)) + } + ast::Expression::Tmpl(ast::Tmpl { tmpl, .. }) => { + let mut str = Vec::new(); + for x in tmpl { + match x { + ast::StringOrExpression::String(x) => str.push(x), + ast::StringOrExpression::Expression(x) => { + let v = self.eval(x, scope).await?; + str.push(v.value.repr_value().to_string()) + } + } + } + Value::str(str.concat()) + } + ast::Expression::Str(ast::Str { value, .. }) => Value::str(value), + ast::Expression::Num(ast::Num { value, .. }) => Value::num(value), + ast::Expression::Bool(ast::Bool { value, .. }) => Value::bool(value), + ast::Expression::Null(_) => Value::null(), + ast::Expression::Obj(ast::Obj { value, .. }) => { + let mut obj = IndexMap::new(); + for (k, v) in value { + obj.insert(k, self.eval(v, scope).await?); + } + Value::obj(obj) + } + ast::Expression::Arr(ast::Arr { value, .. }) => Value::arr( + try_join_all(value.into_iter().map(|node| self.eval(node, scope))).await?, + ), + ast::Expression::Not(ast::Not { expr, .. }) => { + let v = self.eval(*expr, scope).await?; + let bool = bool::try_from(v)?; + Value::bool(!bool) + } + ast::Expression::And(ast::And { left, right, .. }) => { + let Value { + value: left_value, + attr, + } = self.eval(*left, scope).await?; + let left_value = bool::try_from(left_value)?; + if !left_value { + Value { + value: V::Bool(left_value), + attr, + } + } else { + let Value { + value: right_value, + attr, + } = self.eval(*right, scope).await?; + let right_value = bool::try_from(right_value)?; + Value { + value: V::Bool(right_value), + attr, + } + } + } + ast::Expression::Or(ast::Or { left, right, .. }) => { + let Value { + value: left_value, + attr, + } = self.eval(*left, scope).await?; + let left_value = bool::try_from(left_value)?; + if left_value { + Value { + value: V::Bool(left_value), + attr, + } + } else { + let Value { + value: right_value, + attr, + } = self.eval(*right, scope).await?; + let right_value = bool::try_from(right_value)?; + Value { + value: V::Bool(right_value), + attr, + } + } + } + ast::Expression::Identifier(ast::Identifier { name, .. }) => { + scope.get(&name)? + } + ast::Expression::Call(ast::Call { target, args, .. }) => { + let callee = self.eval(*target, scope).await?; + let callee = VFn::try_from(callee)?; + let args = + try_join_all(args.into_iter().map(|node| self.eval(node, scope))) + .await?; + self.fn_(callee, args).await? + } + ast::Expression::Index(ast::Index { target, index, .. }) => { + let target = self.eval(*target, scope).await?; + let i = self.eval(*index, scope).await?; + match target.value { + V::Arr(arr) => { + let i = f64::try_from(i)?; + let item = if i.trunc() == i { + arr.read().unwrap().get(i as usize).cloned() + } else { + None + }; + if let Some(item) = item { + item + } else { + Err(AiScriptRuntimeError::IndexOutOfRange( + i, + arr.read().unwrap().len() as isize - 1, + ))? + } + } + V::Obj(obj) => { + let i = String::try_from(i)?; + if let Some(item) = obj.read().unwrap().get(&i) { + item.clone() + } else { + Value::null() + } + } + target => Err(AiScriptRuntimeError::Runtime(format!( + "Cannot read prop ({}) of {}.", + i.value.repr_value(), + target.display_type(), + )))?, + } + } + ast::Expression::Prop(ast::Prop { target, name, .. }) => { + let value = self.eval(*target.clone(), scope).await?; + if let V::Obj(value) = value.value { + if let Some(value) = value.read().unwrap().get(&name) { + value.clone() + } else { + Value::null() + } + } else { + get_prim_prop(value, name)? + } + } + }, + }) + } + .boxed() + } + + async fn run( + &self, + program: impl IntoIterator>, + scope: &Scope, + ) -> Result { + let mut v = Value::null(); + for node in program { + v = self.eval(node, scope).await?; + if let V::Return(_) | V::Break | V::Continue = v.value { + return Ok(v); + } + } + Ok(v) + } + + pub fn register_abort_handler( + &self, + task: impl Future> + Send + 'static, + ) -> tokio::task::AbortHandle { + self.abort_handlers.lock().unwrap().spawn(task) + } + + pub fn abort(&self) { + self.stop.store(true, Ordering::SeqCst); + self.abort_handlers.lock().unwrap().abort_all(); + } + + fn assign<'a>( + &'a self, + scope: &'a Scope, + dest: ast::Expression, + value: Value, + ) -> BoxFuture<'a, Result<(), AiScriptError>> { + async move { + match dest { + ast::Expression::Identifier(ast::Identifier { name, .. }) => { + scope.assign(name, value)? + } + ast::Expression::Index(ast::Index { target, index, .. }) => { + let assignee = self.eval(*target.clone(), scope).await?; + let i = self.eval(*index, scope).await?; + match assignee.value { + V::Arr(arr) => { + let i = f64::try_from(i)?; + if i.trunc() == i && arr.read().unwrap().get(i as usize).is_some() { + arr.write().unwrap()[i as usize] = value; + } else { + Err(AiScriptRuntimeError::IndexOutOfRange( + i, + arr.read().unwrap().len() as isize - 1, + ))? + } + } + V::Obj(obj) => { + let i = String::try_from(i)?; + obj.write().unwrap().insert(i, value); + } + _ => Err(AiScriptRuntimeError::Runtime(format!( + "Cannot read prop ({}) of {}.", + i.value.repr_value(), + assignee.value.display_type() + )))?, + } + } + ast::Expression::Prop(ast::Prop { target, name, .. }) => { + let assignee = self.eval(*target.clone(), scope).await?; + let assignee = VObj::try_from(assignee.value)?; + assignee.write().unwrap().insert(name, value); + } + ast::Expression::Arr(ast::Arr { value: target, .. }) => { + let value = >::try_from(value)?; + try_join_all(target.into_iter().enumerate().map(|(index, item)| { + self.assign(scope, item, value.get(index).cloned().unwrap_or_default()) + })) + .await?; + } + ast::Expression::Obj(ast::Obj { value: target, .. }) => { + let value = >::try_from(value.value)?; + try_join_all(target.into_iter().map(|(key, item)| { + self.assign(scope, item, value.get(&key).cloned().unwrap_or_default()) + })) + .await?; + } + _ => Err(AiScriptRuntimeError::Runtime( + "The left-hand side of an assignment expression must be \ + a variable or a property/index access." + .to_string(), + ))?, + } + Ok(()) + } + .boxed() + } +} diff --git a/src/interpreter/lib.rs b/src/interpreter/lib.rs new file mode 100644 index 0000000..df1033a --- /dev/null +++ b/src/interpreter/lib.rs @@ -0,0 +1 @@ +pub mod std; diff --git a/src/interpreter/lib/std.rs b/src/interpreter/lib/std.rs new file mode 100644 index 0000000..230f5cd --- /dev/null +++ b/src/interpreter/lib/std.rs @@ -0,0 +1,1528 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, +}; + +use chrono::{Datelike, TimeZone, Timelike}; +use futures::FutureExt; +use indexmap::IndexMap; +use uri_encoding::{decode_uri, decode_uri_component, encode_uri, encode_uri_component}; + +use crate::{ + constants::AISCRIPT_VERSION, + error::{AiScriptError, AiScriptRuntimeError}, + interpreter::{ + lib::std::seedrandom::seedrandom, + util::expect_any, + value::{Value, V}, + }, + values::{VFn, VObj}, +}; + +mod seedrandom; +mod uri_encoding; + +pub fn std() -> HashMap { + let mut std = HashMap::new(); + + std.insert( + "help".to_string(), + Value::str("SEE: https://github.com/syuilo/aiscript/blob/master/docs/get-started.md"), + ); + + std.insert("Core:v".to_string(), Value::str(AISCRIPT_VERSION)); + + std.insert("Core:ai".to_string(), Value::str("kawaii")); + + std.insert( + "Core:not".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = bool::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(!a)) + } + .boxed() + }), + ); + + std.insert( + "Core:eq".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = expect_any(args.next())?; + let b = expect_any(args.next())?; + Ok(Value::bool(a == b)) + } + .boxed() + }), + ); + + std.insert( + "Core:neq".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = expect_any(args.next())?; + let b = expect_any(args.next())?; + Ok(Value::bool(a != b)) + } + .boxed() + }), + ); + + std.insert( + "Core:and".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = bool::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(if !a { + false + } else { + bool::try_from(args.next().unwrap_or_default())? + })) + } + .boxed() + }), + ); + + std.insert( + "Core:or".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = bool::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(if a { + true + } else { + bool::try_from(args.next().unwrap_or_default())? + })) + } + .boxed() + }), + ); + + std.insert( + "Core:add".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(a + b)) + } + .boxed() + }), + ); + + std.insert( + "Core:sub".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(a - b)) + } + .boxed() + }), + ); + + std.insert( + "Core:mul".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(a * b)) + } + .boxed() + }), + ); + + std.insert( + "Core:pow".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + let res = a.powf(b); + if res.is_nan() { + // ex. √-1) + Err(AiScriptRuntimeError::Runtime( + "Invalid operation.".to_string(), + ))? + } else { + Ok(Value::num(res)) + } + } + .boxed() + }), + ); + + std.insert( + "Core:div".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + let res = a / b; + if res.is_nan() { + Err(AiScriptRuntimeError::Runtime( + "Invalid operation.".to_string(), + ))? + } else { + Ok(Value::num(res)) + } + } + .boxed() + }), + ); + + std.insert( + "Core:mod".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(a % b)) + } + .boxed() + }), + ); + + std.insert( + "Core:gt".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(a > b)) + } + .boxed() + }), + ); + + std.insert( + "Core:lt".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(a < b)) + } + .boxed() + }), + ); + + std.insert( + "Core:gteq".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(a >= b)) + } + .boxed() + }), + ); + + std.insert( + "Core:lteq".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(a <= b)) + } + .boxed() + }), + ); + + std.insert( + "Core:type".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = expect_any(args.next())?; + Ok(Value::str(v.value.display_type().to_string())) + } + .boxed() + }), + ); + + std.insert( + "Core:to_str".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = expect_any(args.next())?; + Ok(Value::str(v.value.repr_value().to_string())) + } + .boxed() + }), + ); + + std.insert( + "Core:range".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::arr(if a < b { + let length = (b - a).floor() + 1.0; + let mut i = 0.0; + std::iter::from_fn(move || { + let v = if i < length { Value::num(a + i) } else { None? }; + i += 1.0; + Some(v) + }) + .collect() + } else if a > b { + let length = (a - b).floor() + 1.0; + let mut i = 0.0; + std::iter::from_fn(move || { + let v = if i < length { Value::num(a - i) } else { None? }; + i += 1.0; + Some(v) + }) + .collect() + } else { + vec![Value::num(a)] + })) + } + .boxed() + }), + ); + + std.insert( + "Core:sleep".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let delay = f64::try_from(args.next().unwrap_or_default())?; + tokio::time::sleep(Duration::from_millis(delay as u64)).await; + Ok(Value::null()) + } + .boxed() + }), + ); + + std.insert( + "Core:abort".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let message = String::try_from(args.next().unwrap_or_default())?; + Err(AiScriptRuntimeError::User(message))? + } + .boxed() + }), + ); + + std.insert( + "Util:uuid".to_string(), + Value::fn_native(|_, _| async move { Ok(Value::str(uuid::Uuid::new_v4())) }.boxed()), + ); + + std.insert( + "Json:stringify".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = expect_any(args.next())?; + serde_json::to_string(&v.value).map_or_else( + |err| { + if err.to_string() == "cyclic_reference" { + Err(AiScriptError::Internal("too much recursion".to_string())) + } else { + Ok(Value::error("not_json", None)) + } + }, + |value| Ok(Value::str(value)), + ) + } + .boxed() + }), + ); + + std.insert( + "Json:parse".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let json = String::try_from(args.next().unwrap_or_default())?; + Ok(serde_json::from_str(&json) + .map_or_else(|_| Value::error("not_json", None), Value::new)) + } + .boxed() + }), + ); + + std.insert( + "Json:parsable".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let json = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(serde_json::from_str::(&json).is_ok())) + } + .boxed() + }), + ); + + std.insert( + "Date:now".to_string(), + Value::fn_native(|_, _| { + async move { Ok(Value::num(chrono::Local::now().timestamp_millis() as f64)) }.boxed() + }), + ); + + std.insert( + "Date:year".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let date = + args.next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or_else( + || Ok(chrono::Local::now()), + |v| { + chrono::Local.timestamp_millis_opt(v as i64).single().ok_or( + AiScriptError::Internal(format!("invalid timestamp: {v}")), + ) + }, + )?; + Ok(Value::num(date.year())) + } + .boxed() + }), + ); + + std.insert( + "Date:month".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let date = + args.next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or_else( + || Ok(chrono::Local::now()), + |v| { + chrono::Local.timestamp_millis_opt(v as i64).single().ok_or( + AiScriptError::Internal(format!("invalid timestamp: {v}")), + ) + }, + )?; + Ok(Value::num(date.month())) + } + .boxed() + }), + ); + + std.insert( + "Date:day".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let date = + args.next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or_else( + || Ok(chrono::Local::now()), + |v| { + chrono::Local.timestamp_millis_opt(v as i64).single().ok_or( + AiScriptError::Internal(format!("invalid timestamp: {v}")), + ) + }, + )?; + Ok(Value::num(date.day())) + } + .boxed() + }), + ); + + std.insert( + "Date:hour".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let date = + args.next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or_else( + || Ok(chrono::Local::now()), + |v| { + chrono::Local.timestamp_millis_opt(v as i64).single().ok_or( + AiScriptError::Internal(format!("invalid timestamp: {v}")), + ) + }, + )?; + Ok(Value::num(date.hour())) + } + .boxed() + }), + ); + + std.insert( + "Date:minute".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let date = + args.next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or_else( + || Ok(chrono::Local::now()), + |v| { + chrono::Local.timestamp_millis_opt(v as i64).single().ok_or( + AiScriptError::Internal(format!("invalid timestamp: {v}")), + ) + }, + )?; + Ok(Value::num(date.minute())) + } + .boxed() + }), + ); + + std.insert( + "Date:second".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let date = + args.next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or_else( + || Ok(chrono::Local::now()), + |v| { + chrono::Local.timestamp_millis_opt(v as i64).single().ok_or( + AiScriptError::Internal(format!("invalid timestamp: {v}")), + ) + }, + )?; + Ok(Value::num(date.second())) + } + .boxed() + }), + ); + + std.insert( + "Date:millisecond".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .unwrap_or_else(|| chrono::Local::now().timestamp_millis() as f64); + Ok(Value::num(v % 1000.0)) + } + .boxed() + }), + ); + + std.insert( + "Date:parse".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = String::try_from(args.next().unwrap_or_default())?; + let date = chrono::DateTime::parse_from_rfc3339(&v) + .map_or(f64::NAN, |date| date.timestamp_millis() as f64); + Ok(Value::num(date)) + } + .boxed() + }), + ); + + std.insert( + "Date:to_iso_str".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let mut date = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or_else(chrono::Local::now, |v| { + chrono::Local.timestamp_millis_opt(v as i64).unwrap() + }); + let local_offset = + chrono::Duration::seconds(date.offset().local_minus_utc() as i64); + let ofs = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map(|ofs| chrono::Duration::minutes(ofs as i64)); + if let Some(ofs) = ofs { + date += -local_offset + ofs; + } + let ofs = ofs.unwrap_or(local_offset); + Ok(Value::str(format!( + "{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{ms:03}{offset_s}", + y = date.year(), + mo = date.month(), + d = date.day(), + h = date.hour(), + mi = date.minute(), + s = date.second(), + ms = date.timestamp_millis() % 1000, + offset_s = if ofs.is_zero() { + "Z".to_string() + } else { + format!( + "{hours:+03}:{minutes:02}", + hours = ofs.num_hours(), + minutes = ofs.num_minutes().abs() % 60, + ) + }, + ))) + } + .boxed() + }), + ); + + std.insert("Math:Infinity".to_string(), Value::num(f64::INFINITY)); + + std.insert("Math:E".to_string(), Value::num(std::f64::consts::E)); + + std.insert("Math:LN2".to_string(), Value::num(std::f64::consts::LN_2)); + + std.insert("Math:LN10".to_string(), Value::num(std::f64::consts::LN_10)); + + std.insert( + "Math:LOG2E".to_string(), + Value::num(std::f64::consts::LOG2_E), + ); + + std.insert( + "Math:LOG10E".to_string(), + Value::num(std::f64::consts::LOG10_E), + ); + + std.insert("Math:PI".to_string(), Value::num(std::f64::consts::PI)); + + std.insert( + "Math:SQRT1_2".to_string(), + Value::num(std::f64::consts::FRAC_1_SQRT_2), + ); + + std.insert( + "Math:SQRT2".to_string(), + Value::num(std::f64::consts::SQRT_2), + ); + + std.insert( + "Math:abs".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.abs())) + } + .boxed() + }), + ); + + std.insert( + "Math:acos".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.acos())) + } + .boxed() + }), + ); + + std.insert( + "Math:acosh".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.acosh())) + } + .boxed() + }), + ); + + std.insert( + "Math:asin".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.asin())) + } + .boxed() + }), + ); + + std.insert( + "Math:asinh".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.asinh())) + } + .boxed() + }), + ); + + std.insert( + "Math:atan".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.atan())) + } + .boxed() + }), + ); + + std.insert( + "Math:atanh".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.atanh())) + } + .boxed() + }), + ); + + std.insert( + "Math:atan2".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let y = f64::try_from(args.next().unwrap_or_default())?; + let x = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(y.atan2(x))) + } + .boxed() + }), + ); + + std.insert( + "Math:cbrt".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.cbrt())) + } + .boxed() + }), + ); + + std.insert( + "Math:ceil".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.ceil())) + } + .boxed() + }), + ); + + std.insert( + "Math:clz32".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num((v as i32).leading_zeros())) + } + .boxed() + }), + ); + + std.insert( + "Math:cos".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.cos())) + } + .boxed() + }), + ); + + std.insert( + "Math:cosh".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.cosh())) + } + .boxed() + }), + ); + + std.insert( + "Math:exp".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.exp())) + } + .boxed() + }), + ); + + std.insert( + "Math:expm1".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.exp_m1())) + } + .boxed() + }), + ); + + std.insert( + "Math:floor".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.floor())) + } + .boxed() + }), + ); + + std.insert( + "Math:fround".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v as f32)) + } + .boxed() + }), + ); + + std.insert( + "Math:hypot".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let args = >::try_from(args.next().unwrap_or_default())?; + let len = args.len(); + Ok(Value::num(match len { + 0 => 0.0, + 1 => f64::try_from(args.into_iter().next().unwrap_or_default())?.abs(), + 2 => { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + a.hypot(b) + } + _ => { + let mut values = Vec::new(); + for v in args { + let v = f64::try_from(v)?; + values.push(v); + } + values.iter().fold(0.0, |acc, v| acc + v * v).sqrt() + } + })) + } + .boxed() + }), + ); + + std.insert( + "Math:imul".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num((a as i32) * (b as i32))) + } + .boxed() + }), + ); + + std.insert( + "Math:log".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.ln())) + } + .boxed() + }), + ); + + std.insert( + "Math:log1p".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.ln_1p())) + } + .boxed() + }), + ); + + std.insert( + "Math:log10".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.log10())) + } + .boxed() + }), + ); + + std.insert( + "Math:log2".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.log2())) + } + .boxed() + }), + ); + + std.insert( + "Math:max".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(a.max(b))) + } + .boxed() + }), + ); + + std.insert( + "Math:min".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(a.min(b))) + } + .boxed() + }), + ); + + std.insert( + "Math:pow".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = f64::try_from(args.next().unwrap_or_default())?; + let b = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(a.powf(b))) + } + .boxed() + }), + ); + + std.insert( + "Math:round".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.round())) + } + .boxed() + }), + ); + + std.insert( + "Math:sign".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(if v < 0.0 { + -1.0 + } else if v == 0.0 { + 0.0 + } else { + 1.0 + })) + } + .boxed() + }), + ); + + std.insert( + "Math:sin".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.sin())) + } + .boxed() + }), + ); + + std.insert( + "Math:sinh".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.sinh())) + } + .boxed() + }), + ); + + std.insert( + "Math:sqrt".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.sqrt())) + } + .boxed() + }), + ); + + std.insert( + "Math:tan".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.tan())) + } + .boxed() + }), + ); + + std.insert( + "Math:tanh".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.tanh())) + } + .boxed() + }), + ); + + std.insert( + "Math:trunc".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(v.trunc())) + } + .boxed() + }), + ); + + std.insert( + "Math:rnd".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let min = args.next().and_then(|arg| f64::try_from(arg).ok()); + let max = args.next().and_then(|arg| f64::try_from(arg).ok()); + Ok(Value::num(if let (Some(min), Some(max)) = (min, max) { + let max = max.floor(); + let min = min.ceil(); + (rand::random::() * (max - min + 1.0)).floor() + min + } else { + rand::random() + })) + } + .boxed() + }), + ); + + std.insert( + "Math:gen_rng".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let seed = expect_any(args.next())?; + Ok(match seed.value { + V::Num(num) => Some(num.to_string()), + V::Str(str) => Some(str), + _ => None, + } + .map_or_else(Value::null, |seed| { + let rng = Arc::new(Mutex::new(seedrandom(&seed))); + Value::fn_native(move |args, _| { + let r = (rng.clone().lock().unwrap())(); + async move { + let mut args = args.into_iter(); + let min = args.next().and_then(|arg| f64::try_from(arg).ok()); + let max = args.next().and_then(|arg| f64::try_from(arg).ok()); + Ok(Value::num(if let (Some(min), Some(max)) = (min, max) { + let max = max.floor(); + let min = min.ceil(); + (r * (max - min + 1.0)).floor() + min + } else { + r + })) + } + .boxed() + }) + })) + } + .boxed() + }), + ); + + std.insert( + "Num:to_hex".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = f64::try_from(args.next().unwrap_or_default())?; + Ok(Value::str(format!("{:x}", v as i64))) + } + .boxed() + }), + ); + + std.insert( + "Num:from_hex".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::num( + i64::from_str_radix(&v, 16).map_or(f64::NAN, |v| v as f64), + )) + } + .boxed() + }), + ); + + std.insert("Str:lf".to_string(), Value::str("\n")); + + std.insert( + "Str:lt".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = String::try_from(args.next().unwrap_or_default())?; + let b = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(match a.cmp(&b) { + std::cmp::Ordering::Less => -1.0, + std::cmp::Ordering::Equal => 0.0, + std::cmp::Ordering::Greater => 1.0, + })) + } + .boxed() + }), + ); + + std.insert( + "Str:gt".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let a = String::try_from(args.next().unwrap_or_default())?; + let b = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::num(match a.cmp(&b) { + std::cmp::Ordering::Less => 1.0, + std::cmp::Ordering::Equal => 0.0, + std::cmp::Ordering::Greater => -1.0, + })) + } + .boxed() + }), + ); + + std.insert( + "Str:from_codepoint".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let codepoint = f64::try_from(args.next().unwrap_or_default())?; + char::from_u32(codepoint as u32).map_or_else( + || { + Err(AiScriptError::Internal(format!( + "{codepoint} is not a valid code point" + ))) + }, + |c| Ok(Value::str(c)), + ) + } + .boxed() + }), + ); + + std.insert( + "Str:from_unicode_codepoints".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let codepoints = >::try_from(args.next().unwrap_or_default())?; + let mut s = String::new(); + for codepoint in codepoints { + let codepoint = f64::try_from(codepoint.value)?; + s += char::from_u32(codepoint as u32) + .map_or_else( + || { + Err(AiScriptError::Internal(format!( + "{codepoint} is not a valid code point" + ))) + }, + |c| Ok(c.to_string()), + )? + .as_str(); + } + Ok(Value::str(s)) + } + .boxed() + }), + ); + + std.insert( + "Str:from_utf8_bytes".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let bytes = >::try_from(args.next().unwrap_or_default())?; + let bytes = bytes + .into_iter() + .map(|a| f64::try_from(a).map(|a| a as u8)) + .collect::, AiScriptError>>()?; + Ok(Value::str(String::from_utf8(bytes).unwrap_or_default())) + } + .boxed() + }), + ); + + std.insert( + "Uri:encode_full".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::str(encode_uri(&v))) + } + .boxed() + }), + ); + + std.insert( + "Uri:encode_component".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::str(encode_uri_component(&v))) + } + .boxed() + }), + ); + + std.insert( + "Uri:decode_full".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::str( + decode_uri(&v).map_err(|e| AiScriptError::Internal(e.to_string()))?, + )) + } + .boxed() + }), + ); + + std.insert( + "Uri:decode_component".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let v = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::str( + decode_uri_component(&v).map_err(|e| AiScriptError::Internal(e.to_string()))?, + )) + } + .boxed() + }), + ); + + std.insert( + "Arr:create".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let length = f64::try_from(args.next().unwrap_or_default())?; + let initial = args.next().unwrap_or_default(); + if length < 0.0 { + Err(AiScriptRuntimeError::Runtime( + "arr.repeat expected non-negative number, got negative".to_string(), + ))? + } else if length.trunc() != length { + Err(AiScriptRuntimeError::Runtime( + "arr.repeat expected integer, got non-integer".to_string(), + ))? + } else { + let mut value = Vec::new(); + for _ in 0..length as usize { + value.push(initial.clone()) + } + Ok(Value::arr(value)) + } + } + .boxed() + }), + ); + + std.insert( + "Obj:keys".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let obj = VObj::try_from(args.next().unwrap_or_default())?; + let keys = obj + .read() + .unwrap() + .keys() + .map(Value::str) + .collect::>(); + Ok(Value::arr(keys)) + } + .boxed() + }), + ); + + std.insert( + "Obj:vals".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let obj = VObj::try_from(args.next().unwrap_or_default())?; + let vals = obj + .read() + .unwrap() + .values() + .cloned() + .collect::>(); + Ok(Value::arr(vals)) + } + .boxed() + }), + ); + + std.insert( + "Obj:kvs".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let obj = VObj::try_from(args.next().unwrap_or_default())?; + let kvs = obj + .read() + .unwrap() + .iter() + .map(|(k, v)| Value::arr([Value::str(k), v.clone()])) + .collect::>(); + Ok(Value::arr(kvs)) + } + .boxed() + }), + ); + + std.insert( + "Obj:get".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let obj = VObj::try_from(args.next().unwrap_or_default())?; + let key = String::try_from(args.next().unwrap_or_default())?; + let value = obj.read().unwrap().get(&key).cloned().unwrap_or_default(); + Ok(value) + } + .boxed() + }), + ); + + std.insert( + "Obj:set".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let obj = VObj::try_from(args.next().unwrap_or_default())?; + let key = String::try_from(args.next().unwrap_or_default())?; + let value = expect_any(args.next())?; + obj.write().unwrap().insert(key, value); + Ok(Value::null()) + } + .boxed() + }), + ); + + std.insert( + "Obj:has".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let obj = VObj::try_from(args.next().unwrap_or_default())?; + let key = String::try_from(args.next().unwrap_or_default())?; + let has = obj.read().unwrap().contains_key(&key); + Ok(Value::bool(has)) + } + .boxed() + }), + ); + + std.insert( + "Obj:copy".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let obj = >::try_from(args.next().unwrap_or_default())?; + Ok(Value::obj(obj)) + } + .boxed() + }), + ); + + std.insert( + "Obj:merge".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let mut a = >::try_from(args.next().unwrap_or_default())?; + let b = >::try_from(args.next().unwrap_or_default())?; + a.extend(b); + Ok(Value::obj(a)) + } + .boxed() + }), + ); + + std.insert( + "Error:create".to_string(), + Value::fn_native(|args, _| { + async move { + let mut args = args.into_iter(); + let name = String::try_from(args.next().unwrap_or_default())?; + let info = args.next(); + Ok(Value::error(name, info)) + } + .boxed() + }), + ); + + std.insert( + "Async:interval".to_string(), + Value::fn_native(|args, interpreter| { + let interpreter = interpreter.clone(); + async move { + let mut args = args.into_iter(); + let interval = f64::try_from(args.next().unwrap_or_default())?; + let callback = VFn::try_from(args.next().unwrap_or_default())?; + let immediate = args + .next() + .map(bool::try_from) + .map_or(Ok(None), |r| r.map(Some))?; + let abort_handler = interpreter.register_abort_handler({ + let interpreter = interpreter.clone(); + async move { + let mut interval = + tokio::time::interval(Duration::from_millis(interval as u64)); + if !immediate.unwrap_or(false) { + interval.tick().await; + } + loop { + interval.tick().await; + interpreter.exec_fn(callback.clone(), Vec::new()).await?; + } + } + }); + Ok(Value::fn_native(move |_, _| { + abort_handler.abort(); + async move { Ok(Value::null()) }.boxed() + })) + } + .boxed() + }), + ); + + std.insert( + "Async:timeout".to_string(), + Value::fn_native(|args, interpreter| { + let interpreter = interpreter.clone(); + async move { + let mut args = args.into_iter(); + let interval = f64::try_from(args.next().unwrap_or_default())?; + let callback = VFn::try_from(args.next().unwrap_or_default())?; + let abort_handler = interpreter.register_abort_handler({ + let interpreter = interpreter.clone(); + async move { + tokio::time::sleep(Duration::from_millis(interval as u64)).await; + interpreter.exec_fn(callback.clone(), Vec::new()).await?; + Ok(()) + } + }); + Ok(Value::fn_native(move |_, _| { + abort_handler.abort(); + async move { Ok(Value::null()) }.boxed() + })) + } + .boxed() + }), + ); + + std +} diff --git a/src/interpreter/lib/std/seedrandom.rs b/src/interpreter/lib/std/seedrandom.rs new file mode 100644 index 0000000..5086067 --- /dev/null +++ b/src/interpreter/lib/std/seedrandom.rs @@ -0,0 +1,93 @@ +// https://github.com/davidbau/seedrandom + +const WIDTH: usize = u8::MAX as usize + 1; // each RC4 output is 0 <= x < 256 +const CHUNKS: u32 = 6; // at least six RC4 outputs for each double +const DIGITS: u32 = f64::MANTISSA_DIGITS - 1; // there are 52 significant digits in a double +const STARTDENOM: u64 = (u8::MAX as u64 + 1).pow(CHUNKS); +const SIGNIFICANCE: u64 = 2_u64.pow(DIGITS); +const OVERFLOW: u64 = SIGNIFICANCE * 2; + +pub fn seedrandom(seed: &str) -> impl FnMut() -> f64 { + let key = mixkey(seed); + let mut arc4 = Arc4::new(key); + move || { + let mut n = arc4 + .g(CHUNKS) + .into_iter() + .fold(0_f64, |acc, v| acc * (u8::MAX as f64 + 1.0) + v as f64); + let mut d = STARTDENOM as f64; + let mut x = 0; + while n < SIGNIFICANCE as f64 { + n = (n + x as f64) * WIDTH as f64; + d *= WIDTH as f64; + x = *arc4.g(1).first().unwrap(); + } + while n >= OVERFLOW as f64 { + n /= 2.0; + d /= 2.0; + x >>= 1; + } + (n + x as f64) / d + } +} + +#[derive(Debug, PartialEq, Clone)] +struct Arc4 { + s: [u8; WIDTH], + i: u8, + j: u8, +} + +impl Arc4 { + pub fn new(key: Vec) -> Self { + let key = if key.is_empty() { vec![0] } else { key }; + let keylen = key.len(); + let mut s = [0; WIDTH]; + for i in 0..=u8::MAX { + s[i as usize] = i; + } + let mut j = 0_u8; + for i in 0..=u8::MAX { + let t = s[i as usize]; + j = (j as usize + key[i as usize % keylen] as usize + t as usize) as u8; + s[i as usize] = s[j as usize]; + s[j as usize] = t; + } + let mut arc = Arc4 { s, i: 0, j: 0 }; + arc.g(u8::MAX as u32 + 1); + arc + } + + pub fn g(&mut self, count: u32) -> Vec { + let mut r = Vec::new(); + for _ in 0..count { + let i = (self.i as usize + 1) as u8; + let t = self.s[i as usize]; + let j = (self.j as usize + t as usize) as u8; + self.s[i as usize] = self.s[j as usize]; + self.s[j as usize] = t; + r.push( + self.s[(self.s[i as usize] as usize + self.s[j as usize] as usize) as u8 as usize], + ); + self.i = i; + self.j = j; + } + r + } +} + +fn mixkey(seed: &str) -> Vec { + let mut key = Vec::new(); + let mut smear = 0_u32; + for (j, c) in seed.chars().enumerate() { + let i = j as u8; + smear ^= key.get(i as usize).map_or(0, |i| *i as u32) * 19; + let value = (smear + c as u32) as u8; + if j <= u8::MAX as usize { + key.push(value) + } else { + key[i as usize] = value; + } + } + key +} diff --git a/src/interpreter/lib/std/uri_encoding.rs b/src/interpreter/lib/std/uri_encoding.rs new file mode 100644 index 0000000..da63750 --- /dev/null +++ b/src/interpreter/lib/std/uri_encoding.rs @@ -0,0 +1,59 @@ +// https://tc39.es/ecma262/multipage/global-object.html#sec-uri-handling-functions + +use std::str::Utf8Error; + +use percent_encoding::{ + percent_decode_str, percent_encode_byte, utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC, +}; + +const URI_COMPONENT_ESCAPE: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'_') + .remove(b'-') + .remove(b'.') + .remove(b'!') + .remove(b'~') + .remove(b'*') + .remove(b'\'') + .remove(b'(') + .remove(b')'); + +const URI_ESCAPE: &AsciiSet = &URI_COMPONENT_ESCAPE + .remove(b';') + .remove(b'/') + .remove(b'?') + .remove(b':') + .remove(b'@') + .remove(b'&') + .remove(b'=') + .remove(b'+') + .remove(b'$') + .remove(b',') + .remove(b'#'); + +const PRESERVE_ESCAPE_SET: [u8; 11] = [ + b';', b'/', b'?', b':', b'@', b'&', b'=', b'+', b'$', b',', b'#', +]; + +pub fn encode_uri(input: &str) -> String { + utf8_percent_encode(input, URI_ESCAPE).to_string() +} + +pub fn encode_uri_component(input: &str) -> String { + utf8_percent_encode(input, URI_COMPONENT_ESCAPE).to_string() +} + +pub fn decode_uri(input: &str) -> Result { + let mut bytes = Vec::new(); + for b in percent_decode_str(input) { + if PRESERVE_ESCAPE_SET.contains(&b) { + bytes.extend(percent_encode_byte(b).as_bytes()); + } else { + bytes.push(b); + } + } + std::str::from_utf8(&bytes).map(Into::into) +} + +pub fn decode_uri_component(input: &str) -> Result { + Ok(percent_decode_str(input).decode_utf8()?.to_string()) +} diff --git a/src/interpreter/primitive_props.rs b/src/interpreter/primitive_props.rs new file mode 100644 index 0000000..2452feb --- /dev/null +++ b/src/interpreter/primitive_props.rs @@ -0,0 +1,897 @@ +use futures::{ + future::{try_join_all, BoxFuture}, + try_join, FutureExt, +}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + error::{AiScriptError, AiScriptRuntimeError}, + Interpreter, +}; + +use super::{ + util::expect_any, + value::{VFn, Value, V}, +}; + +pub fn get_prim_prop(target: Value, name: String) -> Result { + Ok(match target.value { + V::Num(target) => match name.as_str() { + "to_str" => Value::fn_native(move |_, _| { + async move { Ok(Value::str(target.to_string())) }.boxed() + }), + _ => Err(AiScriptRuntimeError::Runtime(format!( + "No such prop ({name}) in number." + )))?, + }, + V::Str(target) => match name.as_str() { + "to_num" => Value::fn_native(move |_, _| { + let parsed = target.parse::(); + async move { + Ok(Value::new(parsed.map_or_else( + |_| V::Null, + |parsed| { + if parsed.is_nan() { + V::Null + } else { + V::Num(parsed) + } + }, + ))) + } + .boxed() + }), + "to_arr" => Value::fn_native(move |_, _| { + let arr = target + .graphemes(true) + .map(Value::str) + .collect::>(); + async move { Ok(Value::arr(arr)) }.boxed() + }), + "to_unicode_arr" => Value::fn_native(move |_, _| { + let arr = target.chars().map(Value::str).collect::>(); + async move { Ok(Value::arr(arr)) }.boxed() + }), + "to_unicode_codepoint_arr" => Value::fn_native(move |_, _| { + let arr = target + .chars() + .map(|c| Value::num(c as u32)) + .collect::>(); + async move { Ok(Value::arr(arr)) }.boxed() + }), + "to_char_arr" => Value::fn_native(move |_, _| { + let arr = target + .encode_utf16() + .map(|u| Value::str(String::from_utf16_lossy(&[u]))) + .collect::>(); + async move { Ok(Value::arr(arr)) }.boxed() + }), + "to_charcode_arr" => Value::fn_native(move |_, _| { + let arr = target + .encode_utf16() + .map(Value::num) + .collect::>(); + async move { Ok(Value::arr(arr)) }.boxed() + }), + "to_utf8_byte_arr" => Value::fn_native(move |_, _| { + let arr = target + .as_bytes() + .iter() + .map(|&u| Value::num(u)) + .collect::>(); + async move { Ok(Value::arr(arr)) }.boxed() + }), + "len" => Value::num(target.graphemes(true).count() as f64), + "replace" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let a = String::try_from(args.next().unwrap_or_default())?; + let b = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::str(target.replace(&a, &b))) + } + .boxed() + }), + "index_of" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let search = String::try_from(args.next().unwrap_or_default())?; + let pos = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map(|i| if i < 0.0 { target.len() as f64 + i } else { i }); + Ok(Value::num(if let Some(pos) = pos { + let pos = pos as usize; + let pos = target.grapheme_indices(true).nth(pos).map(|(pos, _)| pos); + if let Some(pos) = pos { + target[pos..].find(&search).map_or(-1.0, |index| { + target + .grapheme_indices(true) + .enumerate() + .find(|(_, (i, _))| index <= *i) + .map_or(-1.0, |(i, _)| (i + pos) as f64) + }) + } else { + -1.0 + } + } else { + target.find(&search).map_or(-1.0, |index| { + target + .grapheme_indices(true) + .enumerate() + .find(|(_, (i, _))| index <= *i) + .map_or(-1.0, |(i, _)| i as f64) + }) + })) + } + .boxed() + }), + "incl" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let search = String::try_from(args.next().unwrap_or_default())?; + Ok(Value::bool(target.contains(&search))) + } + .boxed() + }), + "trim" => Value::fn_native(move |_, _| { + let s = target.trim().to_string(); + async move { Ok(Value::str(s)) }.boxed() + }), + "upper" => Value::fn_native(move |_, _| { + let s = target.to_uppercase(); + async move { Ok(Value::str(s)) }.boxed() + }), + "lower" => Value::fn_native(move |_, _| { + let s = target.to_lowercase(); + async move { Ok(Value::str(s)) }.boxed() + }), + "split" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let splitter = args + .next() + .map(String::try_from) + .map_or(Ok(None), |r| r.map(Some))?; + Ok(Value::arr(match splitter { + Some(splitter) if !splitter.is_empty() => target + .split(&splitter) + .map(Value::str) + .collect::>(), + _ => target + .graphemes(true) + .map(Value::str) + .collect::>(), + })) + } + .boxed() + }), + "slice" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let begin = f64::try_from(args.next().unwrap_or_default())?; + let begin = target + .grapheme_indices(true) + .nth(begin as usize) + .map_or(begin as usize, |(i, _)| i) + .clamp(0, target.len()); + let end = f64::try_from(args.next().unwrap_or_default())?; + let end = target + .grapheme_indices(true) + .nth(end as usize) + .map_or_else(|| target.len(), |(i, _)| i) + .clamp(begin, target.len()); + Ok(Value::str(&target[begin..end])) + } + .boxed() + }), + "pick" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let i = f64::try_from(args.next().unwrap_or_default())?; + Ok(target + .graphemes(true) + .nth(i as usize) + .map_or_else(Value::null, Value::str)) + } + .boxed() + }), + "charcode_at" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let i = f64::try_from(args.next().unwrap_or_default())?; + Ok(target + .encode_utf16() + .map(Value::num) + .nth(i as usize) + .unwrap_or_default()) + } + .boxed() + }), + "codepoint_at" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let i = f64::try_from(args.next().unwrap_or_default())?; + let c = char::decode_utf16(target.encode_utf16().skip(i as usize)) + .map(|r| { + r.map_or_else(|e| e.unpaired_surrogate() as f64, |c| c as u32 as f64) + }) + .next(); + Ok(c.map_or_else(Value::null, Value::num)) + } + .boxed() + }), + "starts_with" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let suffix = String::try_from(args.next().unwrap_or_default())?; + if suffix.is_empty() { + return Ok(Value::bool(true)); + } + + let target_len = target.graphemes(true).count() as isize; + let raw_index = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(|i| Some(i as isize)))? + .unwrap_or(target_len); + if raw_index < -target_len || target_len < raw_index { + return Ok(Value::bool(false)); + } + let index = if raw_index >= 0 { + raw_index + } else { + target_len + raw_index + } as usize; + + Ok(Value::bool( + target[target + .grapheme_indices(true) + .nth(index) + .map_or(0, |(i, _)| i)..] + .starts_with(&suffix), + )) + } + .boxed() + }), + "ends_with" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let suffix = String::try_from(args.next().unwrap_or_default())?; + if suffix.is_empty() { + return Ok(Value::bool(true)); + } + + let target_len = target.graphemes(true).count() as isize; + let raw_index = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(|i| Some(i as isize)))? + .unwrap_or(0); + if raw_index < -target_len || target_len < raw_index { + return Ok(Value::bool(false)); + } + let index = if raw_index >= 0 { + target_len - raw_index + } else { + -raw_index + } as usize; + + Ok(Value::bool( + target[..target + .grapheme_indices(true) + .nth(index) + .map_or_else(|| target.len(), |(index, _)| index)] + .ends_with(&suffix), + )) + } + .boxed() + }), + "pad_start" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let width = f64::try_from(args.next().unwrap_or_default())?; + let pad = args + .next() + .map(String::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .unwrap_or_else(|| " ".to_string()); + let target_len = target.graphemes(true).count(); + let pad_len = pad.graphemes(true).count(); + Ok(Value::str(if width as usize <= target_len { + target + } else { + let width = width as usize - target_len; + let mut s = pad.repeat(width / pad_len); + s += &pad[..pad.grapheme_indices(true).nth(width % pad_len).unwrap().0]; + s += ⌖ + s + })) + } + .boxed() + }), + "pad_end" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let width = f64::try_from(args.next().unwrap_or_default())?; + let pad = args + .next() + .map(String::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .unwrap_or_else(|| " ".to_string()); + let target_len = target.graphemes(true).count(); + let pad_len = pad.graphemes(true).count(); + Ok(Value::str(if width as usize <= target_len { + target + } else { + let width = width as usize - target_len; + let mut s = target; + s += &pad.repeat(width / pad_len); + s += &pad[..pad.grapheme_indices(true).nth(width % pad_len).unwrap().0]; + s + })) + } + .boxed() + }), + _ => Err(AiScriptRuntimeError::Runtime(format!( + "No such prop ({name}) in string." + )))?, + }, + V::Arr(target) => match name.as_str() { + "len" => Value::num(target.read().unwrap().len() as f64), + "push" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let val = expect_any(args.next())?; + target.write().unwrap().push(val); + Ok(Value::new(V::Arr(target))) + } + .boxed() + }), + "unshift" => Value::fn_native(move |args, _| { + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let val = expect_any(args.next())?; + target.write().unwrap().insert(0, val); + Ok(Value::new(V::Arr(target))) + } + .boxed() + }), + "pop" => Value::fn_native(move |_, _| { + let target = target.clone(); + async move { + let val = target.write().unwrap().pop(); + Ok(if let Some(val) = val { + val + } else { + Value::null() + }) + } + .boxed() + }), + "shift" => Value::fn_native(move |_, _| { + let target = target.clone(); + async move { + Ok(if target.read().unwrap().is_empty() { + Value::null() + } else { + target.write().unwrap().remove(0) + }) + } + .boxed() + }), + "concat" => Value::fn_native(move |args, _| { + let mut target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let x = >::try_from(args.next().unwrap_or_default())?; + target.extend(x); + Ok(Value::arr(target)) + } + .boxed() + }), + "slice" => Value::fn_native(move |args, _| { + let target = target.read().unwrap().clone(); + let target_len = target.len(); + async move { + let mut args = args.into_iter(); + let begin = f64::try_from(args.next().unwrap_or_default())?; + let begin = if begin < 0.0 { + (target_len as f64 + begin) as usize + } else { + begin as usize + } + .clamp(0, target_len); + let end = f64::try_from(args.next().unwrap_or_default())?; + let end = if end < 0.0 { + (target_len as f64 + end) as usize + } else { + end as usize + } + .clamp(begin, target_len); + Ok(Value::arr(target[begin..end].iter().cloned())) + } + .boxed() + }), + "join" => Value::fn_native(move |args, _| { + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let joiner = args + .next() + .map(String::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .unwrap_or_else(String::new); + Ok(Value::str( + target + .iter() + .map(|i| { + if let V::Str(value) = &i.value { + value + } else { + "" + } + }) + .collect::>() + .join(&joiner), + )) + } + .boxed() + }), + "map" => Value::fn_native(move |args, interpreter| { + let interpreter = interpreter.clone(); + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let fn_ = VFn::try_from(args.next().unwrap_or_default())?; + Ok(Value::arr( + try_join_all(target.into_iter().enumerate().map(|(i, item)| { + interpreter + .exec_fn_simple(fn_.clone(), vec![item, Value::num(i as f64)]) + })) + .await?, + )) + } + .boxed() + }), + "filter" => Value::fn_native(move |args, interpreter| { + let interpreter = interpreter.clone(); + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let fn_ = VFn::try_from(args.next().unwrap_or_default())?; + let mut vals = Vec::new(); + for (i, item) in target.into_iter().enumerate() { + let res = interpreter + .exec_fn_simple(fn_.clone(), vec![item.clone(), Value::num(i as f64)]) + .await?; + let res = bool::try_from(res)?; + if res { + vals.push(item); + } + } + Ok(Value::arr(vals)) + } + .boxed() + }), + "reduce" => Value::fn_native(move |args, interpreter| { + let interpreter = interpreter.clone(); + let mut target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let fn_ = VFn::try_from(args.next().unwrap_or_default())?; + let initial_value = args.next(); + let with_initial_value = initial_value.is_some(); + if !with_initial_value && target.is_empty() { + Err(AiScriptRuntimeError::Runtime( + "Reduce of empty array without initial value".to_string(), + ))?; + } + let mut accumlator = initial_value.unwrap_or_else(|| target.remove(0)); + for (i, item) in target.into_iter().enumerate() { + accumlator = interpreter + .exec_fn_simple( + fn_.clone(), + vec![ + accumlator, + item, + Value::num(if with_initial_value { i } else { i + 1 } as f64), + ], + ) + .await?; + } + Ok(accumlator) + } + .boxed() + }), + "find" => Value::fn_native(move |args, interpreter| { + let interpreter = interpreter.clone(); + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let fn_ = VFn::try_from(args.next().unwrap_or_default())?; + for (i, item) in target.into_iter().enumerate() { + let res = interpreter + .exec_fn_simple(fn_.clone(), vec![item.clone(), Value::num(i as f64)]) + .await?; + let res = bool::try_from(res)?; + if res { + return Ok(item); + } + } + Ok(Value::null()) + } + .boxed() + }), + "incl" => Value::fn_native(move |args, _| { + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let val = expect_any(args.next())?; + Ok(Value::bool(target.into_iter().any(|item| val == item))) + } + .boxed() + }), + "index_of" => Value::fn_native(move |args, _| { + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let val = expect_any(args.next())?; + let from_i = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or(0.0, |i| if i < 0.0 { target.len() as f64 + i } else { i }) + .clamp(0.0, target.len() as f64) as usize; + Ok(Value::num( + target[from_i..] + .iter() + .position(|item| item == &val) + .map_or(-1.0, |result| (result + from_i) as f64), + )) + } + .boxed() + }), + "reverse" => Value::fn_native(move |_, _| { + target.write().unwrap().reverse(); + async move { Ok(Value::null()) }.boxed() + }), + "copy" => Value::fn_native(move |_, _| { + let target = target.read().unwrap().clone(); + async move { Ok(Value::arr(target)) }.boxed() + }), + "sort" => Value::fn_native({ + fn merge_sort( + arr: Vec, + comp: VFn, + interpreter: &Interpreter, + ) -> BoxFuture<'static, Result, AiScriptError>> { + let len = arr.len(); + if len <= 1 { + return async move { Ok(arr) }.boxed(); + } + let mid = len / 2; + let mut left = arr; + let right = left.split_off(mid); + let interpreter = interpreter.clone(); + async move { + let (left, right) = try_join!( + merge_sort(left, comp.clone(), &interpreter), + merge_sort(right, comp.clone(), &interpreter) + )?; + merge(left, right, comp, &interpreter).await + } + .boxed() + } + async fn merge( + left: Vec, + right: Vec, + comp: VFn, + interpreter: &Interpreter, + ) -> Result, AiScriptError> { + let mut result = Vec::new(); + let mut left_index = 0; + let mut right_index = 0; + while left_index < left.len() && right_index < right.len() { + let l = (left[left_index]).clone(); + let r = (right[right_index]).clone(); + let comp_value = interpreter + .exec_fn_simple(comp.clone(), vec![l.clone(), r.clone()]) + .await?; + let comp_value = f64::try_from(comp_value)?; + if comp_value < 0.0 { + result.push(l); + left_index += 1; + } else { + result.push(r); + right_index += 1; + } + } + result.extend_from_slice(&left[left_index..]); + result.extend_from_slice(&right[right_index..]); + Ok(result) + } + + move |args, interpreter| { + let interpreter = interpreter.clone(); + let target = target.clone(); + async move { + let mut args = args.into_iter(); + let comp = VFn::try_from(args.next().unwrap_or_default())?; + let arr = target.read().unwrap().clone(); + let sorted = merge_sort(arr, comp, &interpreter).await?; + target.write().unwrap().splice(.., sorted); + Ok(Value::new(V::Arr(target))) + } + .boxed() + } + }), + "fill" => Value::fn_native(move |args, _| { + let target = target.clone(); + let target_len = target.read().unwrap().len(); + async move { + let mut args = args.into_iter(); + let val = args.next().unwrap_or_default(); + let start = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or(0, |i| { + if i < 0.0 { + (target_len as f64 + i) as usize + } else { + i as usize + } + }) + .clamp(0, target_len); + let end = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or(target_len, |i| { + if i < 0.0 { + (target_len as f64 + i) as usize + } else { + i as usize + } + }) + .clamp(start, target_len); + for i in start..end { + target.write().unwrap()[i] = val.clone(); + } + Ok(Value::new(V::Arr(target))) + } + .boxed() + }), + "repeat" => Value::fn_native(move |args, _| { + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let times = f64::try_from(args.next().unwrap_or_default())?; + if times < 0.0 { + Err(AiScriptRuntimeError::Runtime( + "arr.repeat expected non-negative number, got negative".to_string(), + ))? + } else if times.trunc() != times { + Err(AiScriptRuntimeError::Runtime( + "arr.repeat expected integer, got non-integer".to_string(), + ))? + } else { + let mut value = Vec::new(); + let target = &target[..]; + for _ in 0..times as usize { + value.extend_from_slice(target) + } + Ok(Value::arr(value)) + } + } + .boxed() + }), + "splice" => Value::fn_native(move |args, _| { + let target = target.clone(); + let target_len = target.read().unwrap().len(); + async move { + let mut args = args.into_iter(); + let idx = f64::try_from(args.next().unwrap_or_default())?; + let index = if idx < 0.0 { + target_len as f64 + idx + } else { + idx + } + .clamp(0.0, target_len as f64) as usize; + let remove_count = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .map_or_else( + || target_len - index, + |rc| rc.clamp(0.0, (target_len - index) as f64) as usize, + ); + let items = args + .next() + .map(>::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .unwrap_or_default(); + let result = target + .write() + .unwrap() + .splice(index..index + remove_count, items) + .collect::>(); + Ok(Value::arr(result)) + } + .boxed() + }), + "flat" => Value::fn_native(move |args, _| { + let mut args = args.into_iter(); + let target = target.read().unwrap().clone(); + async move { + let depth = args + .next() + .map(f64::try_from) + .map_or(Ok(None), |r| r.map(Some))? + .unwrap_or(1.0); + if depth.trunc() != depth { + Err(AiScriptRuntimeError::Runtime( + "arr.flat expected integer, got non-integer".to_string(), + ))? + } else if depth < 0.0 { + Err(AiScriptRuntimeError::Runtime( + "arr.repeat expected non-negative number, got negative".to_string(), + ))? + } else { + fn flat(arr: Vec, depth: usize, result: &mut Vec) { + if depth == 0 { + result.extend(arr); + return; + } + for v in arr { + if let V::Arr(value) = v.value { + flat(value.read().unwrap().clone(), depth - 1, result); + } else { + result.push(v); + } + } + } + let mut result = Vec::new(); + flat(target, depth as usize, &mut result); + Ok(Value::arr(result)) + } + } + .boxed() + }), + "flat_map" => Value::fn_native(move |args, interpreter| { + let interpreter = interpreter.clone(); + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let fn_ = VFn::try_from(args.next().unwrap_or_default())?; + let mapped_vals = + try_join_all(target.into_iter().enumerate().map(|(i, item)| { + interpreter + .exec_fn_simple(fn_.clone(), vec![item, Value::num(i as f64)]) + })) + .await?; + let mut result = Vec::new(); + for value in mapped_vals { + if let V::Arr(value) = value.value { + result.extend(value.read().unwrap().clone()) + } else { + result.push(value) + } + } + Ok(Value::arr(result)) + } + .boxed() + }), + "every" => Value::fn_native(move |args, interpreter| { + let interpreter = interpreter.clone(); + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let fn_ = VFn::try_from(args.next().unwrap_or_default())?; + for (i, item) in target.into_iter().enumerate() { + let res = interpreter + .exec_fn_simple(fn_.clone(), vec![item, Value::num(i as f64)]) + .await?; + let res = bool::try_from(res)?; + if !res { + return Ok(Value::bool(false)); + } + } + Ok(Value::bool(true)) + } + .boxed() + }), + "some" => Value::fn_native(move |args, interpreter| { + let interpreter = interpreter.clone(); + let target = target.read().unwrap().clone(); + async move { + let mut args = args.into_iter(); + let fn_ = VFn::try_from(args.next().unwrap_or_default())?; + for (i, item) in target.into_iter().enumerate() { + let res = interpreter + .exec_fn_simple(fn_.clone(), vec![item, Value::num(i as f64)]) + .await?; + let res = bool::try_from(res)?; + if res { + return Ok(Value::bool(true)); + } + } + Ok(Value::bool(false)) + } + .boxed() + }), + "insert" => Value::fn_native(move |args, _| { + let target = target.clone(); + let target_len = target.read().unwrap().len(); + async move { + let mut args = args.into_iter(); + let idx = f64::try_from(args.next().unwrap_or_default())?; + let index = if idx < 0.0 { + target_len as f64 + idx + } else { + idx + } + .clamp(0.0, target_len as f64) as usize; + let item = expect_any(args.next())?; + target.write().unwrap().insert(index, item); + Ok(Value::null()) + } + .boxed() + }), + "remove" => Value::fn_native(move |args, _| { + let target = target.clone(); + let target_len = target.read().unwrap().len(); + async move { + let mut args = args.into_iter(); + let idx = f64::try_from(args.next().unwrap_or_default())?; + Ok(if target_len == 0 { + Value::null() + } else { + let index = if idx < 0.0 { + target_len as f64 + idx + } else { + idx + } + .clamp(0.0, target_len as f64) as usize; + if index == target_len { + Value::null() + } else { + let removed = target.write().unwrap().remove(index); + removed + } + }) + } + .boxed() + }), + _ => Err(AiScriptRuntimeError::Runtime(format!( + "No such prop ({name}) in string." + )))?, + }, + V::Error { value, info } => match name.as_str() { + "name" => Value::str(value), + "info" => info.map_or_else(Value::null, |info| *info), + _ => Err(AiScriptRuntimeError::Runtime(format!( + "No such prop ({name}) in number." + )))?, + }, + value => Err(AiScriptRuntimeError::Runtime(format!( + "Cannot read prop of {}. (reading {name})", + value.display_type() + )))?, + }) +} diff --git a/src/interpreter/scope.rs b/src/interpreter/scope.rs new file mode 100644 index 0000000..50aa06a --- /dev/null +++ b/src/interpreter/scope.rs @@ -0,0 +1,157 @@ +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use crate::error::{AiScriptError, AiScriptRuntimeError}; + +use super::{value::Value, variable::Variable}; + +#[derive(Debug, Clone)] +pub struct Scope { + parent: Option>, + states: Arc>>, + name: String, + ns_name: Option, +} + +impl Default for Scope { + fn default() -> Self { + Self { + parent: Default::default(), + states: Default::default(), + name: "".to_string(), + ns_name: Default::default(), + } + } +} + +impl Scope { + pub fn new(states: HashMap, name: Option) -> Self { + Scope { + parent: None, + states: Arc::new(RwLock::new(states)), + name: name.unwrap_or_else(|| "".to_string()), + ns_name: None, + } + } + + pub fn create_child_scope( + &self, + states: HashMap, + name: Option, + ) -> Self { + Scope { + parent: Some(self.clone().into()), + states: Arc::new(RwLock::new(states)), + name: name.unwrap_or_else(|| "".to_string()), + ns_name: None, + } + } + + pub fn create_child_namespace_scope( + &self, + ns_name: String, + states: HashMap, + name: Option, + ) -> Self { + Scope { + parent: Some(self.clone().into()), + states: Arc::new(RwLock::new(states)), + name: name.unwrap_or_else(|| "".to_string()), + ns_name: Some(ns_name), + } + } + + pub fn get(&self, name: &str) -> Result { + self.get_(name, &self.name) + } + + fn get_(&self, name: &str, scope_name: &str) -> Result { + if let Some(Variable::Mut(state) | Variable::Const(state)) = + self.states.read().unwrap().get(name) + { + Ok(state.clone()) + } else if let Some(parent) = &self.parent { + parent.get_(name, scope_name) + } else { + Err(AiScriptRuntimeError::Runtime(format!( + "No such variable '{name}' in scope '{scope_name}'", + )))? + } + } + + pub fn exists(&self, name: &str) -> bool { + if self.states.read().unwrap().contains_key(name) { + true + } else if let Some(parent) = &self.parent { + parent.exists(name) + } else { + false + } + } + + pub fn get_all(&self) -> HashMap { + if let Some(parent) = &self.parent { + let mut states = parent.get_all(); + states.extend(self.states.clone().read().unwrap().clone()); + states + } else { + self.states.clone().read().unwrap().clone() + } + } + + pub fn add(&self, name: String, variable: Variable) -> Result<(), AiScriptError> { + if self.states.read().unwrap().contains_key(&name) { + Err(AiScriptRuntimeError::Runtime(format!( + "Variable '{name}' already exists in scope '{}'", + self.name + )))? + } else { + self.states + .write() + .unwrap() + .insert(name.clone(), variable.clone()); + if let Some(parent) = &self.parent { + if let Some(ns_name) = &self.ns_name { + parent.add(format!("{ns_name}:{name}"), variable)?; + } + } + Ok(()) + } + } + + pub fn assign(&self, name: String, val: Value) -> Result<(), AiScriptError> { + self.assign_(name, val, &self.name) + } + + fn assign_(&self, name: String, val: Value, scope_name: &str) -> Result<(), AiScriptError> { + let is_mut = self + .states + .read() + .unwrap() + .get(&name) + .map(|variable| matches!(variable, Variable::Mut(_))); + match is_mut { + Some(true) => { + self.states + .write() + .unwrap() + .insert(name, Variable::Mut(val)); + Ok(()) + } + Some(false) => Err(AiScriptRuntimeError::Runtime(format!( + "Cannot assign to an immutable variable {name}." + )))?, + None => { + if let Some(parent) = &self.parent { + parent.assign_(name, val, scope_name) + } else { + Err(AiScriptRuntimeError::Runtime(format!( + "No such variable '{name}' in scope '{scope_name}" + )))? + } + } + } + } +} diff --git a/src/interpreter/util.rs b/src/interpreter/util.rs new file mode 100644 index 0000000..b22be9d --- /dev/null +++ b/src/interpreter/util.rs @@ -0,0 +1,593 @@ +use std::{ + rc::Rc, + sync::{Arc, RwLock}, +}; + +use indexmap::IndexMap; +use regex::Regex; +use serde::{ + de::Visitor, + ser::{self, SerializeMap, SerializeSeq}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use crate::error::{AiScriptError, AiScriptRuntimeError}; + +use super::value::{VArr, VFn, VObj, Value, V}; + +pub fn expect_any(val: Option) -> Result { + Ok(val.ok_or_else(|| { + AiScriptRuntimeError::Runtime("Expect anything, but got nothing.".to_string()) + })?) +} + +impl TryFrom for bool { + type Error = AiScriptError; + + fn try_from(value: V) -> Result { + if let V::Bool(value) = value { + Ok(value) + } else { + Err(AiScriptRuntimeError::Runtime(format!( + "Expect boolean, but got {}", + value.display_type(), + )))? + } + } +} + +impl TryFrom for bool { + type Error = AiScriptError; + + fn try_from(value: Value) -> Result { + value.value.try_into() + } +} + +impl TryFrom for VFn { + type Error = AiScriptError; + + fn try_from(value: V) -> Result { + if let V::Fn(value) = value { + Ok(value) + } else { + Err(AiScriptRuntimeError::Runtime(format!( + "Expect function, but got {}", + value.display_type(), + )))? + } + } +} + +impl TryFrom for VFn { + type Error = AiScriptError; + + fn try_from(value: Value) -> Result { + value.value.try_into() + } +} + +impl TryFrom for String { + type Error = AiScriptError; + + fn try_from(value: V) -> Result { + if let V::Str(value) = value { + Ok(value) + } else { + Err(AiScriptRuntimeError::Runtime(format!( + "Expect string, but got {}", + value.display_type(), + )))? + } + } +} + +impl TryFrom for String { + type Error = AiScriptError; + + fn try_from(value: Value) -> Result { + value.value.try_into() + } +} + +impl TryFrom for f64 { + type Error = AiScriptError; + + fn try_from(value: V) -> Result { + if let V::Num(value) = value { + Ok(value) + } else { + Err(AiScriptRuntimeError::Runtime(format!( + "Expect number, but got {}", + value.display_type(), + )))? + } + } +} + +impl TryFrom for f64 { + type Error = AiScriptError; + + fn try_from(value: Value) -> Result { + value.value.try_into() + } +} + +impl TryFrom for VObj { + type Error = AiScriptError; + + fn try_from(value: V) -> Result { + if let V::Obj(value) = value { + Ok(value) + } else { + Err(AiScriptRuntimeError::Runtime(format!( + "Expect object, but got {}", + value.display_type(), + )))? + } + } +} + +impl TryFrom for VObj { + type Error = AiScriptError; + + fn try_from(value: Value) -> Result { + value.value.try_into() + } +} + +impl TryFrom for IndexMap { + type Error = AiScriptError; + + fn try_from(value: V) -> Result { + Ok(VObj::try_from(value)?.read().unwrap().clone()) + } +} + +impl TryFrom for IndexMap { + type Error = AiScriptError; + + fn try_from(value: Value) -> Result { + value.value.try_into() + } +} + +impl TryFrom for VArr { + type Error = AiScriptError; + + fn try_from(value: V) -> Result { + if let V::Arr(value) = value { + Ok(value) + } else { + Err(AiScriptRuntimeError::Runtime(format!( + "Expect array, but got {}", + value.display_type(), + )))? + } + } +} + +impl TryFrom for VArr { + type Error = AiScriptError; + + fn try_from(value: Value) -> Result { + value.value.try_into() + } +} + +impl TryFrom for Vec { + type Error = AiScriptError; + + fn try_from(value: V) -> Result { + Ok(VArr::try_from(value)?.read().unwrap().clone()) + } +} + +impl TryFrom for Vec { + type Error = AiScriptError; + + fn try_from(value: Value) -> Result { + value.value.try_into() + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl std::fmt::Display for V { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.display_type().fmt(f)?; + match self { + V::Num(value) => write!(f, "<{}>", value), + V::Bool(value) => write!(f, "<{}>", value), + V::Str(value) => write!(f, "<\"{}\">", value), + V::Fn { .. } => write!(f, "<...>"), + V::Obj(_) => write!(f, "<..>"), + V::Null => write!(f, "<>"), + _ => write!(f, ""), + } + } +} + +impl std::fmt::Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} + +impl V { + pub fn display_type(&self) -> DisplayType<'_> { + DisplayType(self) + } + + pub fn display_simple(&self) -> DisplaySimple<'_> { + DisplaySimple(self) + } +} + +impl Value { + pub fn display_type(&self) -> DisplayType<'_> { + self.value.display_type() + } + + pub fn display_simple(&self) -> DisplaySimple<'_> { + self.value.display_simple() + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct DisplayType<'a>(&'a V); + +impl std::fmt::Display for DisplayType<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self.0 { + V::Null => "null", + V::Bool(_) => "bool", + V::Num(_) => "num", + V::Str(_) => "str", + V::Arr(_) => "arr", + V::Obj(_) => "obj", + V::Fn { .. } => "fn", + V::Return(_) => "return", + V::Break => "break", + V::Continue => "continue", + V::Error { .. } => "error", + } + ) + } +} + +pub struct DisplaySimple<'a>(&'a V); + +impl std::fmt::Display for DisplaySimple<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + V::Num(value) => write!(f, "{}", value), + V::Bool(value) => write!(f, "{}", value), + V::Str(value) => write!(f, "\"{}\"", value), + V::Arr(value) => write!( + f, + "[{}]", + value + .read() + .unwrap() + .iter() + .map(|value| value.display_simple().to_string()) + .collect::>() + .join(", ") + ), + V::Null => write!(f, "(null)"), + v => v.fmt(f), + } + } +} + +impl Serialize for V { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + VWithMemo::new(self.clone()).serialize(serializer) + } +} + +struct VWithMemo { + pub value: V, + pub processed_arrays: Rc>, + pub processed_objects: Rc>, +} + +impl VWithMemo { + pub fn new(value: V) -> Self { + VWithMemo { + value, + processed_arrays: Rc::new(Vec::new()), + processed_objects: Rc::new(Vec::new()), + } + } +} + +impl Serialize for VWithMemo { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match &self.value { + V::Null => serializer.serialize_unit(), + V::Bool(value) => serializer.serialize_bool(*value), + V::Num(value) => { + if value.trunc() == *value { + serializer.serialize_i64(*value as i64) + } else { + serializer.serialize_f64(*value) + } + } + V::Str(value) => serializer.serialize_str(value), + V::Arr(value) => { + if self.processed_arrays.iter().any(|v| Arc::ptr_eq(v, value)) { + Err(ser::Error::custom("cyclic_reference"))? + } else { + let mut processed_arrays = (*self.processed_arrays).clone(); + processed_arrays.push(value.clone()); + let processed_arrays = Rc::new(processed_arrays); + let value = value.read().unwrap(); + let mut seq = serializer.serialize_seq(Some(value.len()))?; + for e in value.iter() { + seq.serialize_element(&VWithMemo { + value: e.value.clone(), + processed_arrays: processed_arrays.clone(), + processed_objects: self.processed_objects.clone(), + })?; + } + seq.end() + } + } + V::Obj(value) => { + if self.processed_objects.iter().any(|v| Arc::ptr_eq(v, value)) { + Err(ser::Error::custom("cyclic_reference")) + } else { + let mut processed_objects = (*self.processed_objects).clone(); + processed_objects.push(value.clone()); + let processed_objects = Rc::new(processed_objects); + let value = value.read().unwrap(); + let mut map = serializer.serialize_map(Some(value.len()))?; + for (k, v) in value.iter() { + map.serialize_entry( + k, + &VWithMemo { + value: v.value.clone(), + processed_arrays: self.processed_arrays.clone(), + processed_objects: processed_objects.clone(), + }, + )?; + } + map.end() + } + } + V::Fn(_) => serializer.serialize_str(""), + value => Err(ser::Error::custom(format!( + "Unrecognized value type: {}", + value.display_type(), + ))), + } + } +} + +impl<'de> Deserialize<'de> for V { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(VVisitor) + } +} + +struct VVisitor; + +impl<'de> Visitor<'de> for VVisitor { + type Value = V; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "V") + } + + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(V::Null) + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + Ok(V::Bool(v)) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(V::Str(v.to_string())) + } + + fn visit_f64(self, v: f64) -> Result + where + E: serde::de::Error, + { + Ok(V::Num(v)) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut arr = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(value) = seq.next_element()? { + arr.push(Value::new(value)); + } + Ok(V::Arr(Arc::new(RwLock::new(arr)))) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut obj = IndexMap::new(); + while let Some((key, value)) = map.next_entry()? { + obj.insert(key, Value::new(value)); + } + Ok(V::Obj(Arc::new(RwLock::new(obj)))) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + self.visit_f64(v as f64) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + self.visit_f64(v as f64) + } +} + +pub fn get_lang_version(input: &str) -> Option { + let re = Regex::new(r"^\s*///\s*@\s*([a-zA-Z0-9_.-]+)(?:[\r\n][\s\S]*)?$").unwrap(); + re.captures(input).map(|captures| captures[1].to_string()) +} + +impl V { + pub fn repr_value(&self) -> ReprValue<'_> { + ReprValue { + value: self, + literal_like: false, + processed_arrays: Rc::new(Vec::new()), + processed_objects: Rc::new(Vec::new()), + } + } + + pub fn literal_like(&self) -> ReprValue<'_> { + ReprValue { + value: self, + literal_like: true, + processed_arrays: Rc::new(Vec::new()), + processed_objects: Rc::new(Vec::new()), + } + } +} + +impl Value { + pub fn repr_value(&self) -> ReprValue<'_> { + self.value.repr_value() + } + + pub fn literal_like(&self) -> ReprValue<'_> { + self.value.literal_like() + } +} + +pub struct ReprValue<'a> { + value: &'a V, + literal_like: bool, + processed_arrays: Rc>, + processed_objects: Rc>, +} + +impl std::fmt::Display for ReprValue<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.value { + V::Str(value) => { + if self.literal_like { + write!( + f, + "\"{}\"", + value + .replace('\\', "\\\\") + .replace('\r', "\\r") + .replace('\n', "\\n") + ) + } else { + write!(f, "{}", value) + } + } + V::Num(value) => write!(f, "{}", value), + V::Arr(value) => { + if self.processed_arrays.iter().any(|v| Arc::ptr_eq(v, value)) { + write!(f, "...") + } else { + let mut processed_arrays = (*self.processed_arrays).clone(); + processed_arrays.push(value); + let processed_arrays = Rc::new(processed_arrays); + write!( + f, + "[ {} ]", + value + .read() + .unwrap() + .iter() + .map(|value| ReprValue { + value: &value.value, + literal_like: true, + processed_arrays: processed_arrays.clone(), + processed_objects: self.processed_objects.clone(), + } + .to_string()) + .collect::>() + .join(", ") + ) + } + } + V::Obj(value) => { + if self.processed_objects.iter().any(|v| Arc::ptr_eq(v, value)) { + write!(f, "...") + } else { + let mut processed_objects = (*self.processed_objects).clone(); + processed_objects.push(value); + let processed_objects = Rc::new(processed_objects); + write!( + f, + "{{ {} }}", + value + .read() + .unwrap() + .iter() + .map(|(key, val)| format!( + "{key}: {}", + ReprValue { + value: &val.value, + literal_like: true, + processed_arrays: self.processed_arrays.clone(), + processed_objects: processed_objects.clone(), + } + )) + .collect::>() + .join(", ") + ) + } + } + V::Bool(value) => write!(f, "{}", value), + V::Null => write!(f, "null"), + V::Fn(value) => write!( + f, + "@( {} ) {{ ... }}", + if let VFn::Fn { args, .. } = value { + args.join(", ") + } else { + String::new() + } + ), + _ => write!(f, "?"), + } + } +} diff --git a/src/interpreter/value.rs b/src/interpreter/value.rs new file mode 100644 index 0000000..65256e7 --- /dev/null +++ b/src/interpreter/value.rs @@ -0,0 +1,189 @@ +use std::sync::{Arc, RwLock}; + +use futures::future::BoxFuture; +use indexmap::IndexMap; + +use crate::{error::AiScriptError, node::StatementOrExpression}; + +use super::{scope::Scope, Interpreter}; + +#[derive(Clone, Debug, Default)] +pub enum V { + #[default] + Null, + Bool(bool), + Num(f64), + Str(String), + Arr(VArr), + Obj(VObj), + Fn(VFn), + Return(Box), + Break, + Continue, + Error { + value: String, + info: Option>, + }, +} + +pub type VArr = Arc>>; + +pub type VObj = Arc>>; + +impl PartialEq for V { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Bool(l0), Self::Bool(r0)) => l0 == r0, + (Self::Num(l0), Self::Num(r0)) => l0 == r0, + (Self::Str(l0), Self::Str(r0)) => l0 == r0, + (Self::Arr(l0), Self::Arr(r0)) => { + l0.read().unwrap().clone() == r0.read().unwrap().clone() + } + (Self::Obj(l0), Self::Obj(r0)) => { + l0.read().unwrap().clone() == r0.read().unwrap().clone() + } + (Self::Fn(_), Self::Fn(_)) => false, + (Self::Return(l0), Self::Return(r0)) => l0 == r0, + ( + Self::Error { + value: l_value, + info: l_info, + }, + Self::Error { + value: r_value, + info: r_info, + }, + ) => l_value == r_value && l_info == r_info, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + +#[derive(Clone)] +pub enum VFn { + Fn { + args: Vec, + statements: Vec, + scope: Scope, + }, + FnNative(VFnNative), +} + +pub type VFnNative = Arc< + dyn Fn(Vec, &Interpreter) -> BoxFuture<'static, Result> + + Sync + + Send, +>; + +impl std::fmt::Debug for VFn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Fn { + args, + statements, + scope, + } => f + .debug_struct("Fn") + .field("args", args) + .field("statements", statements) + .field("scope", scope) + .finish(), + Self::FnNative(_) => f.debug_tuple("FnNative").finish(), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Attr { + pub name: String, + pub value: Value, +} + +#[derive(Clone, Debug, Default)] +pub struct Value { + pub value: V, + pub attr: Option>, +} + +impl Value { + pub const fn new(value: V) -> Self { + Value { value, attr: None } + } + + pub const fn null() -> Self { + Value::new(V::Null) + } + + pub const fn bool(value: bool) -> Self { + Value::new(V::Bool(value)) + } + + pub fn num(value: impl Into) -> Self { + Value::new(V::Num(value.into())) + } + + pub fn str(value: impl Into) -> Self { + Value::new(V::Str(value.into())) + } + + pub fn arr(value: impl IntoIterator) -> Self { + Value::new(V::Arr(Arc::new(RwLock::new(value.into_iter().collect())))) + } + + pub fn obj(value: impl IntoIterator, Value)>) -> Self { + Value::new(V::Obj(Arc::new(RwLock::new( + value + .into_iter() + .map(|(key, value)| (key.into(), value)) + .collect(), + )))) + } + + pub fn fn_( + args: impl IntoIterator>, + statements: impl IntoIterator, + scope: Scope, + ) -> Self { + Value::new(V::Fn(VFn::Fn { + args: args.into_iter().map(Into::into).collect(), + statements: statements.into_iter().collect(), + scope, + })) + } + + pub fn fn_native( + value: impl Fn(Vec, &Interpreter) -> BoxFuture<'static, Result> + + Sync + + Send + + 'static, + ) -> Self { + Value::new(V::Fn(VFn::FnNative(Arc::new(value)))) + } + + pub fn return_(value: Value) -> Self { + Value::new(V::Return(Box::new(value))) + } + + pub fn break_() -> Self { + Value::new(V::Break) + } + + pub fn continue_() -> Self { + Value::new(V::Continue) + } + + pub fn error(value: impl Into, info: Option) -> Self { + Value::new(V::Error { + value: value.into(), + info: info.map(Box::new), + }) + } +} + +pub fn unwrap_ret(v: Value) -> Value { + if let V::Return(value) = v.value { + *value + } else { + v + } +} diff --git a/src/interpreter/variable.rs b/src/interpreter/variable.rs new file mode 100644 index 0000000..512c63d --- /dev/null +++ b/src/interpreter/variable.rs @@ -0,0 +1,7 @@ +use super::value::Value; + +#[derive(Debug, PartialEq, Clone)] +pub enum Variable { + Mut(Value), + Const(Value), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2a57311 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,58 @@ +//! Rust implementation of [AiScript](https://github.com/aiscript-dev/aiscript). +//! +//! # Example +//! +//! ``` +//! use aiscript::{Interpreter, Parser}; +//! # use aiscript::errors::AiScriptError; +//! use futures::FutureExt; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), AiScriptError> { +//! let script = Parser::default().parse("<: 'Hello, world!'")?; +//! let interpreter = Interpreter::new( +//! [], +//! None:: _>, +//! Some(|v| { +//! println!("{v}"); +//! async move {}.boxed() +//! }), +//! None:: _>, +//! None, +//! ); +//! interpreter.exec(script).await?; +//! # Ok(()) +//! # } +//! ``` + +mod constants; +mod error; +mod interpreter; +mod node; +mod parser; +mod r#type; + +pub mod ast { + pub use crate::node::*; +} + +pub mod cst { + pub use crate::parser::node::*; +} + +pub mod errors { + pub use crate::error::*; +} + +pub mod utils { + pub use crate::interpreter::util::*; +} + +pub mod values { + pub use crate::interpreter::value::*; +} + +pub use constants::AISCRIPT_VERSION; +pub use interpreter::scope::Scope; +pub use interpreter::Interpreter; +pub use parser::{Parser, ParserPlugin, PluginType}; diff --git a/src/node.rs b/src/node.rs new file mode 100644 index 0000000..caac63a --- /dev/null +++ b/src/node.rs @@ -0,0 +1,397 @@ +//! ASTノード +//! +//! ASTノードはCSTノードをインタプリタ等から操作しやすい構造に変形したものです。 + +use indexmap::IndexMap; + +#[derive(Debug, PartialEq, Clone)] +pub struct Loc { + pub start: usize, + pub end: usize, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Node { + Namespace(Namespace), + Meta(Meta), + Statement(Statement), + Expression(Expression), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum DefinitionOrNamespace { + Definition(Definition), + Namespace(Namespace), +} + +impl From for Node { + fn from(val: DefinitionOrNamespace) -> Self { + match val { + DefinitionOrNamespace::Definition(definition) => { + Node::Statement(Statement::Definition(definition)) + } + DefinitionOrNamespace::Namespace(namespace) => Node::Namespace(namespace), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum StatementOrExpression { + Statement(Statement), + Expression(Expression), +} + +impl From for Node { + fn from(val: StatementOrExpression) -> Self { + match val { + StatementOrExpression::Statement(statement) => Node::Statement(statement), + StatementOrExpression::Expression(expression) => Node::Expression(expression), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum StringOrExpression { + String(String), + Expression(Expression), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Statement { + Definition(Definition), + Return(Return), + Each(Each), + For(For), + Loop(Loop), + Break(Break), + Continue(Continue), + Assign(Assign), + AddAssign(AddAssign), + SubAssign(SubAssign), +} + +impl From for Node { + fn from(val: Statement) -> Self { + Node::Statement(val) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Expression { + If(If), + Fn(Fn), + Match(Match), + Block(Block), + Exists(Exists), + Tmpl(Tmpl), + Str(Str), + Num(Num), + Bool(Bool), + Null(Null), + Obj(Obj), + Arr(Arr), + Not(Not), + And(And), + Or(Or), + Identifier(Identifier), + Call(Call), + Index(Index), + Prop(Prop), +} + +impl From for Node { + fn from(val: Expression) -> Self { + Node::Expression(val) + } +} + +// 名前空間 +#[derive(Debug, PartialEq, Clone)] +pub struct Namespace { + pub name: String, // 空間名 + pub members: Vec, // メンバー + pub loc: Option, +} + +// メタデータ定義 +#[derive(Debug, PartialEq, Clone)] +pub struct Meta { + pub name: Option, // 名 + pub value: Expression, // 値 + pub loc: Option, +} + +// 変数宣言文 +#[derive(Debug, PartialEq, Clone)] +pub struct Definition { + pub name: String, // 変数名 + pub expr: Expression, // 式 + pub var_type: Option, // 変数の型 + pub mut_: bool, // ミュータブルか否か + pub attr: Option>, // 付加された属性 + pub loc: Option, +} + +// 属性 +#[derive(Debug, PartialEq, Clone)] +pub struct Attribute { + pub name: String, // 属性名 + pub value: Expression, // 値 + pub loc: Option, +} + +// return文 +#[derive(Debug, PartialEq, Clone)] +pub struct Return { + pub expr: Expression, // 式 + pub loc: Option, +} + +// each文 +#[derive(Debug, PartialEq, Clone)] +pub struct Each { + pub var: String, // イテレータ変数名 + pub items: Expression, // 配列 + pub for_: Box, // 本体処理 + pub loc: Option, +} + +// for文 +#[derive(Debug, PartialEq, Clone)] +pub struct For { + pub var: Option, // イテレータ変数名 + pub from: Option, // 開始値 + pub to: Option, // 終値 + pub times: Option, // 回数 + pub for_: Box, // 本体処理 + pub loc: Option, +} + +// loop文 +#[derive(Debug, PartialEq, Clone)] +pub struct Loop { + pub statements: Vec, // 処理 + pub loc: Option, +} + +// break文 +#[derive(Debug, PartialEq, Clone)] +pub struct Break { + pub loc: Option, +} + +// continue文 +#[derive(Debug, PartialEq, Clone)] +pub struct Continue { + pub loc: Option, +} + +// 加算代入文 +#[derive(Debug, PartialEq, Clone)] +pub struct AddAssign { + pub dest: Expression, // 代入先 + pub expr: Expression, // 式 + pub loc: Option, +} + +// 減算代入文 +#[derive(Debug, PartialEq, Clone)] +pub struct SubAssign { + pub dest: Expression, // 代入先 + pub expr: Expression, // 式 + pub loc: Option, +} + +// 代入文 +#[derive(Debug, PartialEq, Clone)] +pub struct Assign { + pub dest: Expression, // 代入先 + pub expr: Expression, // 式 + pub loc: Option, +} + +// 否定 +#[derive(Debug, PartialEq, Clone)] +pub struct Not { + pub expr: Box, // 式 + pub loc: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct And { + pub left: Box, + pub right: Box, + pub operator_loc: Loc, + pub loc: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Or { + pub left: Box, + pub right: Box, + pub operator_loc: Loc, + pub loc: Option, +} + +// if式 +#[derive(Debug, PartialEq, Clone)] +pub struct If { + pub cond: Box, // 条件式 + pub then: Box, // then節 + pub elseif: Vec, + pub else_: Option>, // else節 + pub loc: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Elseif { + pub cond: Expression, // elifの条件式 + pub then: StatementOrExpression, // elif節 +} + +// 関数 +#[derive(Debug, PartialEq, Clone)] +pub struct Fn { + pub args: Vec, + pub ret_type: Option, // 戻り値の型 + pub children: Vec, // 本体処理 + pub loc: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Arg { + pub name: String, // 引数名 + pub arg_type: Option, // 引数の型 +} + +// パターンマッチ +#[derive(Debug, PartialEq, Clone)] +pub struct Match { + pub about: Box, // 対象 + pub qs: Vec, + pub default: Option>, // デフォルト値 + pub loc: Option, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct QA { + pub q: Expression, // 条件 + pub a: StatementOrExpression, // 結果 +} + +// ブロックまたはeval式 +#[derive(Debug, PartialEq, Clone)] +pub struct Block { + pub statements: Vec, + pub loc: Option, +} + +// 変数の存在判定 +#[derive(Debug, PartialEq, Clone)] +pub struct Exists { + pub identifier: Identifier, // 変数名 + pub loc: Option, +} + +// テンプレート +#[derive(Debug, PartialEq, Clone)] +pub struct Tmpl { + pub tmpl: Vec, // 処理 + pub loc: Option, +} + +// 文字列リテラル +#[derive(Debug, PartialEq, Clone)] +pub struct Str { + pub value: String, // 文字列 + pub loc: Option, +} + +// 数値リテラル +#[derive(Debug, PartialEq, Clone)] +pub struct Num { + pub value: f64, // 数値 + pub loc: Option, +} + +// 真理値リテラル +#[derive(Debug, PartialEq, Clone)] +pub struct Bool { + pub value: bool, // 真理値 + pub loc: Option, +} + +// nullリテラル +#[derive(Debug, PartialEq, Clone)] +pub struct Null { + pub loc: Option, +} + +// オブジェクト +#[derive(Debug, PartialEq, Clone)] +pub struct Obj { + pub value: IndexMap, // プロパティ + pub loc: Option, +} + +// 配列 +#[derive(Debug, PartialEq, Clone)] +pub struct Arr { + pub value: Vec, // アイテム + pub loc: Option, +} + +// 変数などの識別子 +#[derive(Debug, PartialEq, Clone)] +pub struct Identifier { + pub name: String, // 変数名 + pub loc: Option, +} + +// 関数呼び出し +#[derive(Debug, PartialEq, Clone)] +pub struct Call { + pub target: Box, // 対象 + pub args: Vec, // 引数 + pub loc: Option, +} + +// 配列要素アクセス +#[derive(Debug, PartialEq, Clone)] +pub struct Index { + pub target: Box, // 対象 + pub index: Box, // インデックス + pub loc: Option, +} + +// プロパティアクセス +#[derive(Debug, PartialEq, Clone)] +pub struct Prop { + pub target: Box, // 対象 + pub name: String, // プロパティ名 + pub loc: Option, +} + +// Type source + +#[derive(Debug, PartialEq, Clone)] +pub enum TypeSource { + NamedTypeSource(NamedTypeSource), + FnTypeSource(FnTypeSource), +} + +// 名前付き型 +#[derive(Debug, PartialEq, Clone)] +pub struct NamedTypeSource { + pub name: String, // 型名 + pub inner: Option>, // 内側の型 + pub loc: Option, +} + +// 関数の型 +#[derive(Debug, PartialEq, Clone)] +pub struct FnTypeSource { + pub args: Vec, // 引数の型 + pub result: Box, // 戻り値の型 + pub loc: Option, +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..e686a13 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,79 @@ +use crate::{ + error::{AiScriptError, AiScriptSyntaxError}, + node as ast, +}; + +use self::{ + node as cst, + parser::parser::{main, preprocess}, + plugins::{ + set_attribute::set_attribute, transform_chain::transform_chain, + validate_keyword::validate_keyword, validate_type::validate_type, + }, +}; + +pub mod node; +#[allow(clippy::module_inception)] +mod parser; +mod plugins; +mod visit; + +pub type ParserPlugin = fn(Vec) -> Result, AiScriptError>; + +pub enum PluginType { + Validate(ParserPlugin), + Transform(ParserPlugin), +} + +struct Plugins { + pub validate: Vec, + pub transform: Vec, +} + +impl Default for Plugins { + fn default() -> Self { + Self { + validate: vec![validate_keyword, validate_type], + transform: vec![set_attribute, transform_chain], + } + } +} + +#[derive(Default)] +pub struct Parser { + plugins: Plugins, +} + +impl Parser { + pub fn new(validate: Vec, transform: Vec) -> Self { + Parser { + plugins: Plugins { + validate, + transform, + }, + } + } + + pub fn parse(&self, input: &str) -> Result, AiScriptError> { + let code = preprocess(input).map_err(AiScriptSyntaxError::Parse)?; + let nodes: Vec = main(&code).map_err(AiScriptSyntaxError::Parse)?; + let nodes = self + .plugins + .validate + .iter() + .try_fold(nodes, |nodes, plugin| plugin(nodes))?; + let nodes = self + .plugins + .transform + .iter() + .try_fold(nodes, |nodes, plugin| plugin(nodes))?; + Ok(nodes.into_iter().map(Into::into).collect()) + } + + pub fn add_plugin(&mut self, plugin: PluginType) { + match plugin { + PluginType::Validate(plugin) => self.plugins.validate.push(plugin), + PluginType::Transform(plugin) => self.plugins.transform.push(plugin), + } + } +} diff --git a/src/parser/node.rs b/src/parser/node.rs new file mode 100644 index 0000000..565fca0 --- /dev/null +++ b/src/parser/node.rs @@ -0,0 +1,762 @@ +//! CSTノード +//! +//! パーサーが生成する直接的な処理結果です。 +//! パーサーが生成しやすい形式になっているため、インタプリタ等では操作しにくい構造になっていることがあります。 +//! この処理結果がプラグインによって処理されるとASTノードとなります。 + +use indexmap::IndexMap; + +use crate::node::{self as ast, Loc}; + +pub use crate::node::{Arg, Break, Continue, FnTypeSource, NamedTypeSource, TypeSource}; + +#[derive(Debug, PartialEq, Clone)] +pub enum Node { + Namespace(Namespace), + Meta(Meta), + Statement(Statement), + Expression(Expression), +} + +impl From for ast::Node { + fn from(val: Node) -> Self { + match val { + Node::Namespace(namespace) => ast::Node::Namespace(namespace.into()), + Node::Meta(meta) => ast::Node::Meta(meta.into()), + Node::Statement(statement) => ast::Node::Statement(statement.into()), + Node::Expression(expression) => ast::Node::Expression(expression.into()), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Statement { + Definition(Definition), + Return(Return), + Attribute(Attribute), // AST + Each(Each), + For(For), + Loop(Loop), + Break(Break), + Continue(Continue), + Assign(Assign), + AddAssign(AddAssign), + SubAssign(SubAssign), +} + +impl From for ast::Statement { + fn from(val: Statement) -> Self { + match val { + Statement::Definition(definition) => ast::Statement::Definition(definition.into()), + Statement::Return(return_) => ast::Statement::Return(return_.into()), + Statement::Attribute(_) => panic!(), + Statement::Each(each) => ast::Statement::Each(each.into()), + Statement::For(for_) => ast::Statement::For(for_.into()), + Statement::Loop(loop_) => ast::Statement::Loop(loop_.into()), + Statement::Break(break_) => ast::Statement::Break(break_), + Statement::Continue(continue_) => ast::Statement::Continue(continue_), + Statement::Assign(assign) => ast::Statement::Assign(assign.into()), + Statement::AddAssign(addassign) => ast::Statement::AddAssign(addassign.into()), + Statement::SubAssign(subassign) => ast::Statement::SubAssign(subassign.into()), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Expression { + Not(Not), + And(And), + Or(Or), + If(If), + Fn(Fn_), + Match(Match), + Block(Block), + Exists(Exists), + Tmpl(Tmpl), + Str(Str), + Num(Num), + Bool(Bool), + Null(Null), + Obj(Obj), + Arr(Arr), + Identifier(Identifier), + Call(Call), // IR + Index(Index), // IR + Prop(Prop), // IR +} + +impl From for ast::Expression { + fn from(val: Expression) -> Self { + match val { + Expression::Not(not) => ast::Expression::Not(not.into()), + Expression::And(and) => ast::Expression::And(and.into()), + Expression::Or(or) => ast::Expression::Or(or.into()), + Expression::If(if_) => ast::Expression::If(if_.into()), + Expression::Fn(fn_) => ast::Expression::Fn(fn_.into()), + Expression::Match(match_) => ast::Expression::Match(match_.into()), + Expression::Block(block) => ast::Expression::Block(block.into()), + Expression::Exists(exists) => ast::Expression::Exists(exists.into()), + Expression::Tmpl(tmpl) => ast::Expression::Tmpl(tmpl.into()), + Expression::Str(str) => ast::Expression::Str(str.into()), + Expression::Num(num) => ast::Expression::Num(num.into()), + Expression::Bool(bool) => ast::Expression::Bool(bool.into()), + Expression::Null(null) => ast::Expression::Null(null.into()), + Expression::Obj(obj) => ast::Expression::Obj(obj.into()), + Expression::Arr(arr) => ast::Expression::Arr(arr.into()), + Expression::Identifier(identifier) => ast::Expression::Identifier(identifier.into()), + Expression::Call(call) => ast::Expression::Call(call.into()), + Expression::Index(index) => ast::Expression::Index(index.into()), + Expression::Prop(prop) => ast::Expression::Prop(prop.into()), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Namespace { + pub name: String, + pub members: Vec, + pub loc: Option, +} + +impl From for ast::Namespace { + fn from(val: Namespace) -> Self { + ast::Namespace { + name: val.name, + members: val.members.into_iter().map(Into::into).collect(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Meta { + pub name: Option, + pub value: Expression, + pub loc: Option, +} + +impl From for ast::Meta { + fn from(val: Meta) -> Self { + ast::Meta { + name: val.name, + value: val.value.into(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Definition { + pub name: String, + pub expr: Expression, + pub var_type: Option, + pub mut_: bool, + pub attr: Option>, // IR + pub loc: Option, +} + +impl From for ast::Definition { + fn from(val: Definition) -> Self { + ast::Definition { + name: val.name, + expr: val.expr.into(), + var_type: val.var_type, + mut_: val.mut_, + attr: val + .attr + .map(|attr| attr.into_iter().map(Into::into).collect()), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Attribute { + pub name: String, + pub value: Expression, + pub loc: Option, +} + +impl From for ast::Attribute { + fn from(val: Attribute) -> Self { + ast::Attribute { + name: val.name, + value: val.value.into(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Return { + pub expr: Expression, + pub loc: Option, +} + +impl From for ast::Return { + fn from(val: Return) -> Self { + ast::Return { + expr: val.expr.into(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Each { + pub var: String, + pub items: Expression, + pub for_: Box, + pub loc: Option, +} + +impl From for ast::Each { + fn from(val: Each) -> Self { + ast::Each { + var: val.var, + items: val.items.into(), + for_: Box::new((*val.for_).into()), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct For { + pub var: Option, + pub from: Option, + pub to: Option, + pub times: Option, + pub for_: Box, + pub loc: Option, +} + +impl From for ast::For { + fn from(val: For) -> Self { + ast::For { + var: val.var, + from: val.from.map(Into::into), + to: val.to.map(Into::into), + times: val.times.map(Into::into), + for_: Box::new((*val.for_).into()), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Loop { + pub statements: Vec, + pub loc: Option, +} + +impl From for ast::Loop { + fn from(val: Loop) -> Self { + ast::Loop { + statements: val.statements.into_iter().map(Into::into).collect(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct AddAssign { + pub dest: Expression, + pub expr: Expression, + pub loc: Option, +} + +impl From for ast::AddAssign { + fn from(val: AddAssign) -> Self { + ast::AddAssign { + dest: val.dest.into(), + expr: val.expr.into(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct SubAssign { + pub dest: Expression, + pub expr: Expression, + pub loc: Option, +} + +impl From for ast::SubAssign { + fn from(val: SubAssign) -> Self { + ast::SubAssign { + dest: val.dest.into(), + expr: val.expr.into(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Assign { + pub dest: Expression, + pub expr: Expression, + pub loc: Option, +} + +impl From for ast::Assign { + fn from(val: Assign) -> Self { + ast::Assign { + dest: val.dest.into(), + expr: val.expr.into(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Not { + pub expr: Box, + pub loc: Option, +} + +impl From for ast::Not { + fn from(val: Not) -> Self { + ast::Not { + expr: Box::new((*val.expr).into()), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct And { + pub left: Box, + pub right: Box, + pub operator_loc: Loc, + pub loc: Option, +} + +impl From for ast::And { + fn from(val: And) -> Self { + ast::And { + left: Box::new((*val.left).into()), + right: Box::new((*val.right).into()), + operator_loc: val.operator_loc, + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Or { + pub left: Box, + pub right: Box, + pub operator_loc: Loc, + pub loc: Option, +} + +impl From for ast::Or { + fn from(val: Or) -> Self { + ast::Or { + left: Box::new((*val.left).into()), + right: Box::new((*val.right).into()), + operator_loc: val.operator_loc, + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct If { + pub cond: Box, + pub then: Box, + pub elseif: Vec, + pub else_: Option>, + pub loc: Option, +} + +impl From for ast::If { + fn from(val: If) -> Self { + ast::If { + cond: Box::new((*val.cond).into()), + then: Box::new((*val.then).into()), + elseif: val.elseif.into_iter().map(Into::into).collect(), + else_: val.else_.map(|else_| Box::new((*else_).into())), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Elseif { + pub cond: Expression, + pub then: StatementOrExpression, +} + +impl From for ast::Elseif { + fn from(val: Elseif) -> Self { + ast::Elseif { + cond: val.cond.into(), + then: val.then.into(), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Fn_ { + pub args: Vec, + pub ret_type: Option, + pub children: Vec, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Fn { + fn from(val: Fn_) -> Self { + ast::Fn { + args: val.args, + ret_type: val.ret_type, + children: val.children.into_iter().map(Into::into).collect(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Match { + pub about: Box, + pub qs: Vec, + pub default: Option>, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Match { + fn from(val: Match) -> Self { + ast::Match { + about: Box::new((*val.about).into()), + qs: val.qs.into_iter().map(Into::into).collect(), + default: val.default.map(|default| Box::new((*default).into())), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct QA { + pub q: Expression, + pub a: StatementOrExpression, +} + +impl From for ast::QA { + fn from(val: QA) -> Self { + ast::QA { + q: val.q.into(), + a: val.a.into(), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Block { + pub statements: Vec, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Block { + fn from(val: Block) -> Self { + ast::Block { + statements: val.statements.into_iter().map(Into::into).collect(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Exists { + pub identifier: Identifier, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Exists { + fn from(val: Exists) -> Self { + ast::Exists { + identifier: val.identifier.into(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Tmpl { + pub tmpl: Vec, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Tmpl { + fn from(val: Tmpl) -> Self { + ast::Tmpl { + tmpl: val.tmpl.into_iter().map(Into::into).collect(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Str { + pub value: String, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Str { + fn from(val: Str) -> Self { + ast::Str { + value: val.value, + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Num { + pub value: f64, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Num { + fn from(val: Num) -> Self { + ast::Num { + value: val.value, + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Bool { + pub value: bool, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Bool { + fn from(val: Bool) -> Self { + ast::Bool { + value: val.value, + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Null { + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Null { + fn from(val: Null) -> Self { + ast::Null { loc: val.loc } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Obj { + pub value: IndexMap, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Obj { + fn from(val: Obj) -> Self { + ast::Obj { + value: val + .value + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Arr { + pub value: Vec, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Arr { + fn from(val: Arr) -> Self { + ast::Arr { + value: val.value.into_iter().map(Into::into).collect(), + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Identifier { + pub name: String, + pub chain: Option>, + pub loc: Option, +} + +impl From for ast::Identifier { + fn from(val: Identifier) -> Self { + ast::Identifier { + name: val.name, + loc: val.loc, + } + } +} + +// AST +#[derive(Debug, PartialEq, Clone)] +pub enum ChainMember { + CallChain(CallChain), + IndexChain(IndexChain), + PropChain(PropChain), +} + +// AST +#[derive(Debug, PartialEq, Clone)] +pub struct CallChain { + pub args: Vec, + pub loc: Option, +} + +// AST +#[derive(Debug, PartialEq, Clone)] +pub struct IndexChain { + pub index: Expression, + pub loc: Option, +} + +// AST +#[derive(Debug, PartialEq, Clone)] +pub struct PropChain { + pub name: String, + pub loc: Option, +} + +// IR +#[derive(Debug, PartialEq, Clone)] +pub struct Call { + pub target: Box, + pub args: Vec, + pub loc: Option, +} + +impl From for ast::Call { + fn from(val: Call) -> Self { + ast::Call { + target: Box::new((*val.target).into()), + args: val.args.into_iter().map(Into::into).collect(), + loc: val.loc, + } + } +} + +// IR +#[derive(Debug, PartialEq, Clone)] +pub struct Index { + pub target: Box, + pub index: Box, + pub loc: Option, +} + +impl From for ast::Index { + fn from(val: Index) -> Self { + Self { + target: Box::new((*val.target).into()), + index: Box::new((*val.index).into()), + loc: val.loc, + } + } +} + +// IR +#[derive(Debug, PartialEq, Clone)] +pub struct Prop { + pub target: Box, + pub name: String, + pub loc: Option, +} + +impl From for ast::Prop { + fn from(val: Prop) -> Self { + Self { + target: Box::new((*val.target).into()), + name: val.name, + loc: val.loc, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum DefinitionOrNamespace { + Definition(Definition), + Namespace(Namespace), +} + +impl From for ast::DefinitionOrNamespace { + fn from(val: DefinitionOrNamespace) -> Self { + match val { + DefinitionOrNamespace::Definition(definition) => { + ast::DefinitionOrNamespace::Definition(definition.into()) + } + DefinitionOrNamespace::Namespace(namespace) => { + ast::DefinitionOrNamespace::Namespace(namespace.into()) + } + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum StatementOrExpression { + Statement(Statement), + Expression(Expression), +} + +impl From for ast::StatementOrExpression { + fn from(val: StatementOrExpression) -> Self { + match val { + StatementOrExpression::Statement(statement) => { + ast::StatementOrExpression::Statement(statement.into()) + } + StatementOrExpression::Expression(expression) => { + ast::StatementOrExpression::Expression(expression.into()) + } + } + } +} + +impl From for Node { + fn from(val: StatementOrExpression) -> Self { + match val { + StatementOrExpression::Statement(statement) => Node::Statement(statement), + StatementOrExpression::Expression(expression) => Node::Expression(expression), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum StringOrExpression { + String(String), + Expression(Expression), +} + +impl From for ast::StringOrExpression { + fn from(val: StringOrExpression) -> Self { + match val { + StringOrExpression::String(string) => ast::StringOrExpression::String(string), + StringOrExpression::Expression(expression) => { + ast::StringOrExpression::Expression(expression.into()) + } + } + } +} diff --git a/src/parser/parser.rs b/src/parser/parser.rs new file mode 100644 index 0000000..bdd0fbf --- /dev/null +++ b/src/parser/parser.rs @@ -0,0 +1,1147 @@ +use indexmap::IndexMap; + +use crate::node::Loc; + +use super::node::*; + +peg::parser! { + pub grammar parser() for str { + // + // preprocessor + // + + pub rule preprocess() -> String + = s:preprocess_part()* { s.join("") } + + rule preprocess_part() -> String + = text:$(tmpl()) { text.to_string() } + / text:$(str()) { text.to_string() } + / comment() + / c:[_] { c.to_string() } + + rule comment() -> String + = text:$("//" (!eol() [_])*) { " ".repeat(text.len()) } + / text:$("/*" (!"*/" [_])* "*/") { text.replace( |c| c != '\n', " ") } + + // + // main parser + // + + pub rule main() -> Vec + = _* content:global_statements()? _* { content.unwrap_or_default() } + + rule global_statements() -> Vec + = global_statement() ++ (__* lf() _*) + + rule namespace_statements() -> Vec + = namespace_statement() ++ (__* lf() _*) + + rule statements() -> Vec + = statement() ++ (__* lf() _*) + + // list of global statements + + rule global_statement() -> Node + = namespace:namespace() { Node::Namespace(namespace) } // "::" + / meta:meta() { Node::Meta(meta) } // "###" + / statement:statement() { statement.into() } + + // list of namespace statement + + rule namespace_statement() -> DefinitionOrNamespace + = var_def:var_def() { DefinitionOrNamespace::Definition(var_def) } + / fn_def:fn_def() { DefinitionOrNamespace::Definition(fn_def) } + / namespace:namespace() { DefinitionOrNamespace::Namespace(namespace) } + + // list of statement + + rule statement() -> StatementOrExpression + = var_def:var_def() { StatementOrExpression::Statement(Statement::Definition(var_def)) } // "let" NAME | "var" NAME + / fn_def:fn_def() { StatementOrExpression::Statement(Statement::Definition(fn_def)) } // "@" + / out:out() { StatementOrExpression::Expression(Expression::Identifier(out)) } // "<:" + / return_:return() { StatementOrExpression::Statement(Statement::Return(return_)) } // "return" + / attr:attr() { StatementOrExpression::Statement(Statement::Attribute(attr)) } // "+" + / each:each() { StatementOrExpression::Statement(Statement::Each(each)) } // "each" + / for_:for() { StatementOrExpression::Statement(Statement::For(for_)) } // "for" + / loop_:loop() { StatementOrExpression::Statement(Statement::Loop(loop_)) } // "loop" + / break_:break() { StatementOrExpression::Statement(Statement::Break(break_)) } // "break" + / continue_:continue() { StatementOrExpression::Statement(Statement::Continue(continue_)) } // "continue" + / add_assign:add_assign() { StatementOrExpression::Statement(Statement::AddAssign(add_assign)) } // Expr "+=" + / sub_assign:sub_assign() { StatementOrExpression::Statement(Statement::SubAssign(sub_assign)) } // Expr "-=" + / assign:assign() { StatementOrExpression::Statement(Statement::Assign(assign)) } // Expr "=" + / expr:expr() { StatementOrExpression::Expression(expr) } + + // list of expression + + #[cache] + rule expr() -> Expression + = start:position!() expression:(precedence! { + left:(@) infix_sp()* start:position!() "&&" end:position!() infix_sp()* right:@ { + ( + Expression::And( + And { + left: left.0.into(), + right: right.0.into(), + operator_loc: Loc{ start, end: end - 1 }, + loc: None, + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() "||" end:position!() infix_sp()* right:@ { + ( + Expression::Or( + Or { + left: left.0.into(), + right: right.0.into(), + operator_loc: Loc{ start, end: end - 1 }, + loc: None, + } + ), + true, + ) + } + -- + left:(@) infix_sp()* start:position!() "<=" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:lteq".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() ">=" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:gteq".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + -- + left:(@) infix_sp()* start:position!() "==" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:eq".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() "!=" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:neq".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() "<" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:lt".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() ">" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:gt".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + -- + left:(@) infix_sp()* start:position!() "+" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:add".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() "-" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:sub".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + -- + left:(@) infix_sp()* start:position!() "*" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:mul".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() "^" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:pow".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() "/" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:div".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + left:(@) infix_sp()* start:position!() "%" end:position!() infix_sp()* right:@ { + ( + Expression::Identifier( + Identifier { + name: "Core:mod".to_string(), + chain: Some( + vec![ChainMember::CallChain(CallChain { + args: vec![left.0, right.0], + loc: None, + })] + ), + loc: Some(Loc{ start, end: end - 1 }), + } + ), + true, + ) + } + -- + e:expr2() { (e, false) } + }) end:position!() { + match expression { + (Expression::Identifier(identifier), true) => Expression::Identifier( + Identifier { + chain: identifier.chain.map(|mut chain| { + if let Some(ChainMember::CallChain(call_chain)) = chain.first() { + chain[0] = ChainMember::CallChain( + CallChain { + args: call_chain.args.clone(), + loc: call_chain.loc.clone().or(Some(Loc { start, end })), + } + ); + } + chain + }), + ..identifier + } + ), + (Expression::And(and), true) => Expression::And( + And { + loc: and.loc.clone().or(Some(Loc { start, end })), + ..and + } + ), + (Expression::Or(or), true) => Expression::Or( + Or { + loc: or.loc.clone().or(Some(Loc { start, end })), + ..or + } + ), + (expression, _) => expression, + } + } + + rule expr2() -> Expression + = if_:if() { Expression::If(if_) } // "if" + / fn_:fn() { Expression::Fn(fn_) } // "@(" + / chain() // Expr3 "(" | Expr3 "[" | Expr3 "." + / expr3() + + rule expr3() -> Expression + = match_:match() { Expression::Match(match_) } // "match" + / eval:eval() { Expression::Block(eval) } // "eval" + / exists:exists() { Expression::Exists(exists) } // "exists" + / tmpl:tmpl() { Expression::Tmpl(tmpl) } // "`" + / str:str() { Expression::Str(str) } // "\"" + / num:num() { Expression::Num(num) } // "+" | "-" | "1"~"9" + / bool:bool() { Expression::Bool(bool) } // "true" | "false" + / null:null() { Expression::Null(null) } // "null" + / obj:obj() { Expression::Obj(obj) } // "{" + / arr:arr() { Expression::Arr(arr) } // "[" + / not:not() { Expression::Not(not) } // "!" + / identifier:identifier() { Expression::Identifier(identifier) } // NAME_WITH_NAMESPACE + / "(" _* e:expr() _* ")" { e } + + // list of static literal + + rule static_literal() -> Expression + = num:num() {Expression::Num(num)} + / str:str() {Expression::Str(str)} + / bool:bool() {Expression::Bool(bool)} + / static_arr:static_arr() {Expression::Arr(static_arr)} + / static_obj:static_obj() {Expression::Obj(static_obj)} + / null:null() {Expression::Null(null)} + + // + // global statements --------------------------------------------------------------------- + // + + // namespace statement + + rule namespace() -> Namespace + = start:position!() "::" _+ name:name() _+ "{" _* members:namespace_statements()? _* "}" end:position!() { + Namespace { + name, + members: members.unwrap_or_default(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // meta statement + + rule meta() -> Meta + = start:position!() "###" __* name:name() _* value:static_literal() end:position!() { + Meta { + name: Some(name), + value, + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() "###" __* value:static_literal() end:position!() { + Meta { + name: None, + value, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // + // statements ---------------------------------------------------------------------------- + // + + // define statement + + rule var_def() -> Definition + = start:position!() "let" _+ name:name() type_:(_* ":" _* type_:type_() { type_ })? _* "=" _* expr:expr() end:position!() { + Definition { + name, + var_type: type_, + expr, + mut_: false, + attr: Some(Vec::new()), + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() "var" _+ name:name() type_:(_* ":" _* type_:type_() { type_ })? _* "=" _* expr:expr() end:position!() { + Definition { + name, + var_type: type_, + expr, + mut_: true, + attr: Some(Vec::new()), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // output statement + + // NOTE: out is syntax sugar for print(expr) + rule out() -> Identifier + = start:position!() "<:" _* expr:expr() end:position!() { + Identifier { + name: "print".to_string(), + chain: Some( + vec![ + ChainMember::CallChain( + CallChain { args: vec![expr], loc: Some(Loc{ start, end: end - 1 }) }, + ), + ], + ), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // attribute statement + + // Note: Attribute will be combined with def node when parsing is complete. + rule attr() -> Attribute + = start:position!() "#[" _* name:name() value:(_* value:static_literal() { value })? _* "]" end:position!() { + Attribute { + name, + value: value.unwrap_or_else(|| Expression::Bool(Bool { value: true, chain: None, loc: None})), + loc: Some(Loc{ start, end: end - 1 }) + } + } + + // each statement + + rule each() -> Each + = start:position!() "each" _* "(" "let" _+ varn:name() _* ","? _* items:expr() ")" _* x:block_or_statement() end:position!() { + Each { + var: varn, + items, + for_: x.into(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() "each" _+ "let" _+ varn:name() _* ","? _* items:expr() _+ x:block_or_statement() end:position!() { + Each { + var: varn, + items, + for_: x.into(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // for statement + + rule for() -> For + = start:position!() "for" _* "(" "let" _+ varn:name() _* from_:("=" _* v:expr() { v })? ","? _* to:expr() ")" _* x:block_or_statement() end:position!() { + For { + var: Some(varn), + from: Some(from_.unwrap_or_else(|| Expression::Num(Num { value: 0.0, chain: None, loc: None }))), + to: Some(to), + times: None, + for_: x.into(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() "for" _+ "let" _+ varn:name() _* from_:("=" _* v:expr() { v })? ","? _* to:expr() _+ x:block_or_statement() end:position!() { + For { + var: Some(varn), + from: Some(from_.unwrap_or_else(|| Expression::Num(Num { value: 0.0, chain: None, loc: None }))), + to: Some(to), + times: None, + for_: x.into(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() "for" _* "(" times:expr() ")" _* x:block_or_statement() end:position!() { + For { + var: None, + from: None, + to: None, + times: Some(times), + for_: x.into(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() "for" _+ times:expr() _+ x:block_or_statement() end:position!() { + For { + var: None, + from: None, + to: None, + times: Some(times), + for_: x.into(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule return() -> Return + = start:position!() "return" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] _* expr:expr() end:position!() { + Return { + expr, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule loop() -> Loop + = start:position!() "loop" _* "{" _* s:statements() _* "}" end:position!() { + Loop { + statements: s, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule break() -> Break + = start:position!() "break" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] end:position!() { + Break { + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule continue() -> Continue + = start:position!() "continue" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] end:position!() { + Continue { + loc: Some(Loc{ start, end: end - 1 }) + } + } + + rule add_assign() -> AddAssign + = start:position!() dest:expr() _* "+=" _* expr:expr() end:position!() { + AddAssign { + dest, + expr, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule sub_assign() -> SubAssign + = start:position!() dest:expr() _* "-=" _* expr:expr() end:position!() { + SubAssign { + dest, + expr, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule assign() -> Assign + = start:position!() dest:expr() _* "=" _* expr:expr() end:position!() { + Assign { + dest, + expr, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // + // expressions -------------------------------------------------------------------- + // + + // infix expression + + rule infix_sp() + = "\\" lf() + / __ + + rule not() -> Not + = start:position!() "!" expr:expr() end:position!() { + Not { + expr: expr.into(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // chain + + rule chain() -> Expression + = e:expr3() chain:chain_member()+ { + match e { + Expression::Not(_) => e, + Expression::And(_) => e, + Expression::Or(_) => e, + Expression::If(_) => e, + Expression::Fn(fn_) => { + let mut c = fn_.chain.unwrap_or_default(); + c.extend(chain); + Expression::Fn(Fn_ { + chain: Some(c), + ..fn_ + }) + }, + Expression::Match(match_) => { + let mut c = match_.chain.unwrap_or_default(); + c.extend(chain); + Expression::Match(Match { + chain: Some(c), + ..match_ + }) + }, + Expression::Block(block) => { + let mut c = block.chain.unwrap_or_default(); + c.extend(chain); + Expression::Block(Block { + chain: Some(c), + ..block + }) + }, + Expression::Exists(exists) => { + let mut c = exists.chain.unwrap_or_default(); + c.extend(chain); + Expression::Exists(Exists { + chain: Some(c), + ..exists + }) + }, + Expression::Tmpl(tmpl) => { + let mut c = tmpl.chain.unwrap_or_default(); + c.extend(chain); + Expression::Tmpl(Tmpl { + chain: Some(c), + ..tmpl + }) + }, + Expression::Str(str) => { + let mut c = str.chain.unwrap_or_default(); + c.extend(chain); + Expression::Str(Str { + chain: Some(c), + ..str + }) + }, + Expression::Num(num) => { + let mut c = num.chain.unwrap_or_default(); + c.extend(chain); + Expression::Num(Num { + chain: Some(c), + ..num + }) + }, + Expression::Bool(bool) => { + let mut c = bool.chain.unwrap_or_default(); + c.extend(chain); + Expression::Bool(Bool { + chain: Some(c), + ..bool + }) + }, + Expression::Null(null) => { + let mut c = null.chain.unwrap_or_default(); + c.extend(chain); + Expression::Null(Null { + chain: Some(c), + ..null + }) + }, + Expression::Obj(obj) => { + let mut c = obj.chain.unwrap_or_default(); + c.extend(chain); + Expression::Obj(Obj { + chain: Some(c), + ..obj + }) + }, + Expression::Arr(arr) => { + let mut c = arr.chain.unwrap_or_default(); + c.extend(chain); + Expression::Arr(Arr { + chain: Some(c), + ..arr + }) + }, + Expression::Identifier(identifier) => { + let mut c = identifier.chain.unwrap_or_default(); + c.extend(chain); + Expression::Identifier(Identifier { + chain: Some(c), + ..identifier + }) + }, + Expression::Call(_) => e, + Expression::Index(_) => e, + Expression::Prop(_) => e, + } + } + + rule chain_member() -> ChainMember + = call_chain:call_chain() { ChainMember::CallChain(call_chain) } + / index_chain:index_chain() { ChainMember::IndexChain(index_chain) } + / prop_chain:prop_chain() { ChainMember::PropChain(prop_chain) } + + rule call_chain() -> CallChain + = start:position!() "(" _* args:call_args()? _* ")" end:position!() { + CallChain { + args: args.unwrap_or_default(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule call_args() -> Vec + = expr() ++ sep() + + rule index_chain() -> IndexChain + = start:position!() "[" _* index:expr() _* "]" end:position!() { + IndexChain { + index, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule prop_chain() -> PropChain + = start:position!() "." name:name() end:position!() { + PropChain { + name, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // if statement + + rule if() -> If + = start:position!() + "if" _+ cond:expr() _+ + then:block_or_statement() + elseif:(_+ elseif_blocks:elseif_blocks() { elseif_blocks })? + else_block:(_+ else_block:else_block() { else_block })? end:position!() { + If { + cond: cond.into(), + then: then.into(), + elseif: elseif.unwrap_or_default(), + else_: else_block.map(Into::into), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule elseif_blocks() -> Vec + = elseif_block() ++ (_*) + + rule elseif_block() -> Elseif + = "elif" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] _* cond:expr() _* then:block_or_statement() { + Elseif { cond, then } + } + + rule else_block() -> StatementOrExpression + = "else" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] _* then:block_or_statement() { + then + } + + // match expression + + rule match() -> Match + = start:position!() "match" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] _* + about:expr() _* + "{" _* + qs:(q:expr() _* "=>" _* a:block_or_statement() _* { QA{ q, a } })+ + x:("*" _* "=>" _* x:block_or_statement() { x })? _* + "}" end:position!() { + Match { + about: about.into(), + qs, + default: x.map(Into::into), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // eval expression + + rule eval() -> Block + = start:position!() "eval" _* "{" _* s:statements() _* "}" end:position!() { + Block { + statements: s, + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // exists expression + + rule exists() -> Exists + = start:position!() "exists" _+ i:identifier() end:position!() { + Exists { + identifier: i, + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // variable reference expression + + rule identifier() -> Identifier + = start:position!() name:name_with_namespace() end:position!() { + Identifier { + name, + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // + // literals ------------------------------------------------------------------------------ + // + + // template literal + + rule tmpl() -> Tmpl + = start:position!() "`" items:(!"`" tmpl_embed:tmpl_embed() { tmpl_embed })* "`" end:position!() { + Tmpl{ + tmpl: items, + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule tmpl_embed() -> StringOrExpression + = "{" __* expr:expr() __* "}" { StringOrExpression::Expression(expr) } + / str:tmpl_atom()+ { StringOrExpression::String(str.into_iter().collect() ) } + + rule tmpl_atom() -> char + = tmpl_esc() + / c:([^'`' | '{']) {c} + + rule tmpl_esc() -> char + = "\\" esc:['{' | '}' | '`'] { esc } + + // string literal + + rule str() -> Str + = start:position!() "\"" value:(!"\"" c:(str_double_quote_esc() / [_]) {c})* "\"" end:position!() { + Str { + value: value.into_iter().collect(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() "'" value:(!"'" c:(str_single_quote_esc() / [_]) {c})* "'" end:position!() { + Str { + value: value.into_iter().collect(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule str_double_quote_esc() -> char + = r#"\""# {'"'} + + rule str_single_quote_esc() -> char + = r#"\'"# {'\''} + + // number literal + + rule num() -> Num + = float() + / int() + + rule float() -> Num + = start:position!() n:$(['+' | '-']? ['1'..='9'] ['0'..='9']+ "." ['0'..='9']+) end:position!() { + Num { + value: n.parse().unwrap(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() n:$(['+' | '-']? ['0'..='9'] "." ['0'..='9']+) end:position!() { + Num { + value: n.parse().unwrap(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule int() -> Num + = start:position!() n:$(['+' | '-']? ['1'..='9'] ['0'..='9']+) end:position!() { + Num { + value: n.parse().unwrap(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() n:$(['+' | '-']? ['0'..='9']) end:position!() { + Num { + value: n.parse().unwrap(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // boolean literal + + rule bool() -> Bool + = true() + / false() + + rule true() -> Bool + = start:position!() "true" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] end:position!() { + Bool { + value: true, + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule false() -> Bool + = start:position!() "false" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] end:position!() { + Bool { + value: false, + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // null literal + + rule null() -> Null + = start:position!() "null" !['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':'] end:position!() { + Null { + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // object literal + + rule obj() -> Obj + = start:position!() "{" _* kvs:(k:name() _* ":" _+ v:expr() _* ("," / ";")? _* { (k, v) })* _* "}" end:position!() { + Obj { + value: IndexMap::from_iter(kvs), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // array literal + + rule arr() -> Arr + = start:position!() "[" _* items:(item:expr() _* ","? _* { item })* _* "]" end:position!() { + Arr { + value: items, + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // + // function ------------------------------------------------------------------------------ + // + + rule arg() -> Arg + = name:name() type_:(_* ":" _* type_:type_() { type_ })? { + Arg { name, arg_type: type_ } + } + + rule args() -> Vec + = arg() ++ sep() + + // define function statement + + rule fn_def() -> Definition + = start:position!() + "@" + (quiet!{ !(__+) } / expected!("Cannot use spaces before or after the function name.")) + name:name() + (quiet!{ !(__+) } / expected!("Cannot use spaces before or after the function name.")) + "(" _* + args:args()? _* + ")" + ret:(_* ":" _* type_:type_() { type_ })? _* + "{" _* + content:statements()? _* + "}" + end:position!() { + Definition { + name, + expr: Expression::Fn( + Fn_ { + args: args.unwrap_or_default(), + ret_type: ret, + children: content.unwrap_or_default(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + }, + ), + var_type: None, + mut_: false, + attr: Some(Vec::new()), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // function expression + + rule fn() -> Fn_ + = start:position!() + "@(" _* args:args()? _* ")" + ret:(_* ":" _* type_:type_() { type_ })? _* + "{" _* content:statements()? _* "}" + end:position!() { + Fn_ { + args: args.unwrap_or_default(), + ret_type: ret, + children: content.unwrap_or_default(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // + // static literal ------------------------------------------------------------------------ + // + + // array literal (static) + + rule static_arr() -> Arr + = start:position!() "[" _* items:(item:static_literal() _* ","? _* { item })* _* "]" end:position!() { + Arr { + value: items, + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // object literal (static) + + rule static_obj() -> Obj + = start:position!() "{" _* kvs:(k:name() _* ":" _+ v:static_literal() _* ("," / ";")? _* { (k, v) })* "}" end:position!() { + Obj { + value: IndexMap::from_iter(kvs), + chain: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // + // type ---------------------------------------------------------------------------------- + // + + rule type_() -> TypeSource + = fn_type:fn_type() { TypeSource::FnTypeSource(fn_type) } + / named_type:named_type() { TypeSource::NamedTypeSource(named_type) } + + rule fn_type() -> FnTypeSource + = start:position!() "@(" _* args:arg_types()? _* ")" _* "=>" _* result:type_() end:position!() { + FnTypeSource{ + args: args.unwrap_or_default(), + result: result.into(), + loc: Some(Loc{ start, end: end - 1 }), + } + } + + rule arg_types() -> Vec + = type_() ++ sep() + + rule named_type() -> NamedTypeSource + = start:position!() name:name() __* "<" __* inner:type_() __* ">" end:position!() { + NamedTypeSource { + name, + inner: Some(inner.into()), + loc: Some(Loc{ start, end: end - 1 }), + } + } + / start:position!() name:name() end:position!() { + NamedTypeSource { + name, + inner: None, + loc: Some(Loc{ start, end: end - 1 }), + } + } + + // + // general ------------------------------------------------------------------------------- + // + + rule name() -> String + = text:$(['a'..='z' | 'A'..='Z' | '_'] ['a'..='z' | 'A'..='Z' | '0'..='9' | '_']*) { + text.to_string() + } + + rule name_with_namespace() -> String + = text:$(name() ++ ":") { text.to_string() } + + rule sep() + = _* "," _* + / _+ + + rule block_or_statement() -> StatementOrExpression + = start:position!() "{" _* s:statements()? _* "}" end:position!() { + StatementOrExpression::Expression( + Expression::Block( + Block { + statements: s.unwrap_or_default(), + chain: None, + loc: Some(Loc{ start, end: end - 1 }) + } + ) + ) + } + / statement() + + rule lf() + = "\r\n" / ['\r' | '\n'] + + rule eol() + = ![_] / lf() + + // spacing + rule _() -> char + = [' ' | '\t' | '\r' | '\n'] + + // spacing (no linebreaks) + rule __() -> char + = [' ' | '\t'] + } +} diff --git a/src/parser/plugins.rs b/src/parser/plugins.rs new file mode 100644 index 0000000..a354d61 --- /dev/null +++ b/src/parser/plugins.rs @@ -0,0 +1,4 @@ +pub mod set_attribute; +pub mod transform_chain; +pub mod validate_keyword; +pub mod validate_type; diff --git a/src/parser/plugins/set_attribute.rs b/src/parser/plugins/set_attribute.rs new file mode 100644 index 0000000..d499bc7 --- /dev/null +++ b/src/parser/plugins/set_attribute.rs @@ -0,0 +1,102 @@ +use crate::{error::AiScriptError, errors::AiScriptSyntaxError, parser::node as cst}; + +pub fn set_attribute( + nodes: impl IntoIterator, +) -> Result, AiScriptError> { + let mut result = Vec::::new(); + let mut statements = Vec::::new(); + + for node in nodes { + match node { + cst::Node::Statement(statement) => { + statements.push(cst::StatementOrExpression::Statement(statement)) + } + cst::Node::Expression(expression) => { + statements.push(cst::StatementOrExpression::Expression(expression)) + } + _ => { + if !statements.is_empty() { + let mut nodes = set_attribute_statement_or_expression(statements.clone())? + .into_iter() + .map(Into::into) + .collect::>(); + result.append(&mut nodes); + statements.clear(); + } + result.push(node); + } + }; + } + if !statements.is_empty() { + let mut nodes = set_attribute_statement_or_expression(statements.clone())? + .into_iter() + .map(Into::into) + .collect::>(); + result.append(&mut nodes); + } + + Ok(result) +} + +fn set_attribute_statement_or_expression( + nodes: impl IntoIterator, +) -> Result, AiScriptError> { + let mut result = Vec::::new(); + let mut stocked_attrs = Vec::::new(); + + for node in nodes { + match node { + cst::StatementOrExpression::Statement(cst::Statement::Attribute(attribute)) => { + stocked_attrs.push(attribute); + } + cst::StatementOrExpression::Statement(cst::Statement::Definition(definition)) => { + let mut attr = definition.attr.unwrap_or_default(); + attr.extend(stocked_attrs.splice(.., [])); + let definition = cst::Definition { + attr: Some(attr), + expr: if let cst::Expression::Fn(fn_) = definition.expr { + cst::Expression::Fn(cst::Fn_ { + children: set_attribute_statement_or_expression(fn_.children)?, + ..fn_ + }) + } else { + definition.expr + }, + ..definition + }; + result.push(cst::StatementOrExpression::Statement( + cst::Statement::Definition(definition), + )); + } + _ => { + if !stocked_attrs.is_empty() { + Err(AiScriptSyntaxError::Attribute)? + } + let node = match node { + cst::StatementOrExpression::Expression(expression) => { + cst::StatementOrExpression::Expression(match expression { + cst::Expression::Fn(fn_) => cst::Expression::Fn(cst::Fn_ { + children: set_attribute_statement_or_expression(fn_.children)?, + ..fn_ + }), + cst::Expression::Block(block) => cst::Expression::Block(cst::Block { + statements: set_attribute_statement_or_expression( + block.statements, + )?, + ..block + }), + _ => expression, + }) + } + _ => node, + }; + result.push(node); + } + } + } + if !stocked_attrs.is_empty() { + Err(AiScriptSyntaxError::Attribute)? + } + + Ok(result) +} diff --git a/src/parser/plugins/transform_chain.rs b/src/parser/plugins/transform_chain.rs new file mode 100644 index 0000000..c525d25 --- /dev/null +++ b/src/parser/plugins/transform_chain.rs @@ -0,0 +1,143 @@ +use crate::{ + error::AiScriptError, + parser::{node as cst, visit::Visitor}, +}; + +#[derive(Debug, PartialEq, Clone)] +struct ChainTransformer; + +impl Visitor for ChainTransformer { + fn callback_expression( + &self, + expression: cst::Expression, + ) -> Result { + // chain + match &expression { + cst::Expression::Fn(cst::Fn_ { + chain: Some(chain), .. + }) + | cst::Expression::Match(cst::Match { + chain: Some(chain), .. + }) + | cst::Expression::Block(cst::Block { + chain: Some(chain), .. + }) + | cst::Expression::Exists(cst::Exists { + chain: Some(chain), .. + }) + | cst::Expression::Tmpl(cst::Tmpl { + chain: Some(chain), .. + }) + | cst::Expression::Str(cst::Str { + chain: Some(chain), .. + }) + | cst::Expression::Num(cst::Num { + chain: Some(chain), .. + }) + | cst::Expression::Bool(cst::Bool { + chain: Some(chain), .. + }) + | cst::Expression::Null(cst::Null { + chain: Some(chain), .. + }) + | cst::Expression::Obj(cst::Obj { + chain: Some(chain), .. + }) + | cst::Expression::Arr(cst::Arr { + chain: Some(chain), .. + }) + | cst::Expression::Identifier(cst::Identifier { + chain: Some(chain), .. + }) => Ok(chain.iter().fold( + match &expression { + cst::Expression::Not(not) => cst::Expression::Not(not.clone()), + cst::Expression::And(and) => cst::Expression::And(and.clone()), + cst::Expression::Or(or) => cst::Expression::Or(or.clone()), + cst::Expression::If(if_) => cst::Expression::If(if_.clone()), + cst::Expression::Fn(fn_) => cst::Expression::Fn(cst::Fn_ { + chain: None, + ..fn_.clone() + }), + cst::Expression::Match(match_) => cst::Expression::Match(cst::Match { + chain: None, + ..match_.clone() + }), + cst::Expression::Block(block) => cst::Expression::Block(cst::Block { + chain: None, + ..block.clone() + }), + cst::Expression::Exists(exists) => cst::Expression::Exists(cst::Exists { + chain: None, + ..exists.clone() + }), + cst::Expression::Tmpl(tmpl) => cst::Expression::Tmpl(cst::Tmpl { + chain: None, + ..tmpl.clone() + }), + cst::Expression::Str(str) => cst::Expression::Str(cst::Str { + chain: None, + ..str.clone() + }), + cst::Expression::Num(num) => cst::Expression::Num(cst::Num { + chain: None, + ..num.clone() + }), + cst::Expression::Bool(bool) => cst::Expression::Bool(cst::Bool { + chain: None, + ..bool.clone() + }), + cst::Expression::Null(null) => cst::Expression::Null(cst::Null { + chain: None, + ..null.clone() + }), + cst::Expression::Obj(obj) => cst::Expression::Obj(cst::Obj { + chain: None, + ..obj.clone() + }), + cst::Expression::Arr(arr) => cst::Expression::Arr(cst::Arr { + chain: None, + ..arr.clone() + }), + cst::Expression::Identifier(identifier) => { + cst::Expression::Identifier(cst::Identifier { + chain: None, + ..identifier.clone() + }) + } + cst::Expression::Call(call) => cst::Expression::Call(call.clone()), + cst::Expression::Index(index) => cst::Expression::Index(index.clone()), + cst::Expression::Prop(prop) => cst::Expression::Prop(prop.clone()), + }, + |parent, chain_member| match chain_member { + cst::ChainMember::CallChain(call_chain) => cst::Expression::Call(cst::Call { + target: parent.into(), + args: call_chain.args.clone(), + loc: call_chain.loc.clone(), + }), + cst::ChainMember::IndexChain(index_chain) => { + cst::Expression::Index(cst::Index { + target: parent.into(), + index: index_chain.index.clone().into(), + loc: index_chain.loc.clone(), + }) + } + cst::ChainMember::PropChain(prop_chain) => cst::Expression::Prop(cst::Prop { + target: parent.into(), + name: prop_chain.name.clone(), + loc: prop_chain.loc.clone(), + }), + }, + )), + _ => Ok(expression), + } + } +} + +pub fn transform_chain( + nodes: impl IntoIterator, +) -> Result, AiScriptError> { + nodes + .into_iter() + .map(|node| ChainTransformer.visit_node(node)) + .collect() +} diff --git a/src/parser/plugins/validate_keyword.rs b/src/parser/plugins/validate_keyword.rs new file mode 100644 index 0000000..90feca2 --- /dev/null +++ b/src/parser/plugins/validate_keyword.rs @@ -0,0 +1,136 @@ +use crate::{ + error::{AiScriptError, AiScriptSyntaxError}, + parser::{node as cst, visit::Visitor}, +}; + +const RESERVED_WORD: [&str; 29] = [ + "null", + "true", + "false", + "each", + "for", + "loop", + "break", + "continue", + "match", + "if", + "elif", + "else", + "return", + "eval", + "var", + "let", + "exists", + // future + "fn", + "namespace", + "meta", + "attr", + "attribute", + "static", + "class", + "struct", + "module", + "while", + "import", + "export", + // "const", + // "def", + // "func", + // "function", + // "ref", + // "out", +]; + +#[derive(Debug, PartialEq, Clone)] +struct KeywordValidator; + +impl Visitor for KeywordValidator { + fn callback_namespace( + &self, + namespace: cst::Namespace, + ) -> Result { + if RESERVED_WORD.contains(&namespace.name.as_str()) { + Err(AiScriptSyntaxError::ReservedWord(namespace.name))? + } else { + Ok(namespace) + } + } + + fn callback_meta(&self, meta: cst::Meta) -> Result { + match meta { + cst::Meta { + name: Some(name), .. + } if RESERVED_WORD.contains(&name.as_str()) => { + Err(AiScriptSyntaxError::ReservedWord(name.to_string()))? + } + _ => Ok(meta), + } + } + + fn callback_statement( + &self, + statement: cst::Statement, + ) -> Result { + match &statement { + cst::Statement::Definition(cst::Definition { name, .. }) + | cst::Statement::Attribute(cst::Attribute { name, .. }) => { + if RESERVED_WORD.contains(&name.as_str()) { + Err(AiScriptSyntaxError::ReservedWord(name.to_string()))? + } else { + Ok(statement) + } + } + _ => Ok(statement), + } + } + + fn callback_expression( + &self, + expression: cst::Expression, + ) -> Result { + match &expression { + cst::Expression::Identifier(cst::Identifier { name, .. }) => { + if RESERVED_WORD.contains(&name.as_str()) { + Err(AiScriptSyntaxError::ReservedWord(name.to_string()))? + } else { + Ok(expression) + } + } + cst::Expression::Fn(cst::Fn_ { args, .. }) => { + for arg in args { + if RESERVED_WORD.contains(&arg.name.as_str()) { + Err(AiScriptSyntaxError::ReservedWord(arg.name.to_string()))? + } + } + Ok(expression) + } + _ => Ok(expression), + } + } + + fn callback_chain_member( + &self, + chain_member: cst::ChainMember, + ) -> Result { + match &chain_member { + cst::ChainMember::PropChain(cst::PropChain { name, .. }) => { + if RESERVED_WORD.contains(&name.as_str()) { + Err(AiScriptSyntaxError::ReservedWord(name.to_string()))? + } else { + Ok(chain_member) + } + } + _ => Ok(chain_member), + } + } +} + +pub fn validate_keyword( + nodes: impl IntoIterator, +) -> Result, AiScriptError> { + nodes + .into_iter() + .map(|node| KeywordValidator.visit_node(node)) + .collect() +} diff --git a/src/parser/plugins/validate_type.rs b/src/parser/plugins/validate_type.rs new file mode 100644 index 0000000..21855b8 --- /dev/null +++ b/src/parser/plugins/validate_type.rs @@ -0,0 +1,47 @@ +use crate::{ + error::AiScriptError, + parser::{node as cst, visit::Visitor}, + r#type::get_type_by_source, +}; + +#[derive(Debug, PartialEq, Clone)] +struct TypeValidator; + +impl Visitor for TypeValidator { + fn callback_statement( + &self, + statement: cst::Statement, + ) -> Result { + if let cst::Statement::Definition(cst::Definition { + var_type: Some(var_type), + .. + }) = &statement + { + get_type_by_source(var_type.clone())?; + }; + Ok(statement) + } + + fn callback_expression( + &self, + expression: cst::Expression, + ) -> Result { + if let cst::Expression::Fn(cst::Fn_ { + ret_type: Some(ret_type), + .. + }) = &expression + { + get_type_by_source(ret_type.clone())?; + }; + Ok(expression) + } +} + +pub fn validate_type( + nodes: impl IntoIterator, +) -> Result, AiScriptError> { + nodes + .into_iter() + .map(|node| TypeValidator.visit_node(node)) + .collect() +} diff --git a/src/parser/visit.rs b/src/parser/visit.rs new file mode 100644 index 0000000..ffd56a9 --- /dev/null +++ b/src/parser/visit.rs @@ -0,0 +1,504 @@ +use indexmap::IndexMap; + +use crate::{error::AiScriptError, parser::node as cst}; + +pub trait Visitor { + fn visit_node(&self, node: cst::Node) -> Result { + match node { + cst::Node::Namespace(namespace) => { + self.visit_namespace(namespace).map(cst::Node::Namespace) + } + cst::Node::Meta(meta) => self.visit_meta(meta).map(cst::Node::Meta), + cst::Node::Statement(statement) => { + self.visit_statement(statement).map(cst::Node::Statement) + } + cst::Node::Expression(expression) => { + self.visit_expression(expression).map(cst::Node::Expression) + } + } + } + + fn visit_namespace(&self, namespace: cst::Namespace) -> Result { + let namespace = self.callback_namespace(namespace)?; + Ok(cst::Namespace { + members: namespace + .members + .into_iter() + .map(|member| match member { + cst::DefinitionOrNamespace::Definition(definition) => self + .visit_statement(cst::Statement::Definition(definition)) + .map(|statement| { + if let cst::Statement::Definition(definition) = statement { + cst::DefinitionOrNamespace::Definition(definition) + } else { + panic!() + } + }), + cst::DefinitionOrNamespace::Namespace(namespace) => self + .visit_namespace(namespace) + .map(cst::DefinitionOrNamespace::Namespace), + }) + .collect::, AiScriptError>>()?, + ..namespace + }) + } + + fn visit_meta(&self, meta: cst::Meta) -> Result { + let meta = self.callback_meta(meta)?; + Ok(meta) + } + + fn visit_statement(&self, statement: cst::Statement) -> Result { + let statement = self.callback_statement(statement)?; + Ok(match statement { + cst::Statement::Definition(definition) => cst::Statement::Definition(cst::Definition { + expr: self.visit_expression(definition.expr)?, + ..definition + }), + cst::Statement::Return(return_) => cst::Statement::Return(cst::Return { + expr: self.visit_expression(return_.expr)?, + ..return_ + }), + cst::Statement::Attribute(_) => statement, + cst::Statement::Each(each) => cst::Statement::Each(cst::Each { + items: self.visit_expression(each.items)?, + for_: match *each.for_ { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement)?, + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression)?, + } + .into(), + ..each + }), + cst::Statement::For(for_) => cst::Statement::For(cst::For { + from: for_ + .from + .map(|expression| self.visit_expression(expression)) + .map_or(Ok(None), |r| r.map(Some))?, + to: for_ + .to + .map(|expression| self.visit_expression(expression)) + .map_or(Ok(None), |r| r.map(Some))?, + times: for_ + .times + .map(|expression| self.visit_expression(expression)) + .map_or(Ok(None), |r| r.map(Some))?, + for_: match *for_.for_ { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement)?, + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression)?, + } + .into(), + ..for_ + }), + cst::Statement::Loop(loop_) => cst::Statement::Loop(cst::Loop { + statements: loop_ + .statements + .into_iter() + .map(|s| match s { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement), + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression), + }) + .collect::, AiScriptError>>()?, + ..loop_ + }), + cst::Statement::Break(_) => statement, + cst::Statement::Continue(_) => statement, + cst::Statement::Assign(assign) => cst::Statement::Assign(cst::Assign { + expr: self.visit_expression(assign.expr)?, + dest: self.visit_expression(assign.dest)?, + ..assign + }), + cst::Statement::AddAssign(add_assign) => cst::Statement::AddAssign(cst::AddAssign { + expr: self.visit_expression(add_assign.expr)?, + dest: self.visit_expression(add_assign.dest)?, + ..add_assign + }), + cst::Statement::SubAssign(sub_assign) => cst::Statement::SubAssign(cst::SubAssign { + expr: self.visit_expression(sub_assign.expr)?, + dest: self.visit_expression(sub_assign.dest)?, + ..sub_assign + }), + }) + } + + fn visit_expression( + &self, + expression: cst::Expression, + ) -> Result { + let expression = self.callback_expression(expression)?; + Ok(match expression { + cst::Expression::Not(not) => cst::Expression::Not(cst::Not { + expr: self.visit_expression(*not.expr)?.into(), + ..not + }), + cst::Expression::And(and) => cst::Expression::And(cst::And { + left: self.visit_expression(*and.left)?.into(), + right: self.visit_expression(*and.right)?.into(), + ..and + }), + cst::Expression::Or(or) => cst::Expression::Or(cst::Or { + left: self.visit_expression(*or.left)?.into(), + right: self.visit_expression(*or.right)?.into(), + ..or + }), + cst::Expression::If(if_) => cst::Expression::If(cst::If { + cond: self.visit_expression(*if_.cond)?.into(), + then: match *if_.then { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement)?, + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression)?, + } + .into(), + elseif: if_ + .elseif + .into_iter() + .map(|elseif| { + Ok(cst::Elseif { + cond: self.visit_expression(elseif.cond)?, + then: match elseif.then { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement)?, + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression)?, + }, + }) + }) + .collect::, AiScriptError>>()?, + else_: if_ + .else_ + .map(|else_| match *else_ { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement), + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression), + }) + .map_or(Ok(None), |r| r.map(Some))? + .map(Into::into), + ..if_ + }), + cst::Expression::Fn(fn_) => cst::Expression::Fn(cst::Fn_ { + children: fn_ + .children + .into_iter() + .map(|child| match child { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement), + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression), + }) + .collect::, AiScriptError>>()?, + chain: fn_ + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..fn_ + }), + cst::Expression::Match(match_) => cst::Expression::Match(cst::Match { + about: self.visit_expression(*match_.about)?.into(), + qs: match_ + .qs + .into_iter() + .map(|cst::QA { q, a }| { + Ok(cst::QA { + q: self.visit_expression(q)?, + a: match a { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement)?, + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression)?, + }, + }) + }) + .collect::, AiScriptError>>()?, + default: match_ + .default + .map(|default| match *default { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement), + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression), + }) + .map_or(Ok(None), |r| r.map(Some))? + .map(Into::into), + chain: match_ + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..match_ + }), + cst::Expression::Block(block) => cst::Expression::Block(cst::Block { + statements: block + .statements + .into_iter() + .map(|child| match child { + cst::StatementOrExpression::Statement(statement) => self + .visit_statement(statement) + .map(cst::StatementOrExpression::Statement), + cst::StatementOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StatementOrExpression::Expression), + }) + .collect::, AiScriptError>>()?, + chain: block + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..block + }), + cst::Expression::Exists(exists) => cst::Expression::Exists(cst::Exists { + identifier: self + .visit_expression(cst::Expression::Identifier(exists.identifier)) + .map(|expression| { + if let cst::Expression::Identifier(identifier) = expression { + identifier + } else { + panic!() + } + })?, + chain: exists + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..exists + }), + cst::Expression::Tmpl(tmpl) => cst::Expression::Tmpl(cst::Tmpl { + tmpl: tmpl + .tmpl + .into_iter() + .map(|tmpl| match tmpl { + cst::StringOrExpression::String(_) => Ok(tmpl), + cst::StringOrExpression::Expression(expression) => self + .visit_expression(expression) + .map(cst::StringOrExpression::Expression), + }) + .collect::, AiScriptError>>()?, + chain: tmpl + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..tmpl + }), + cst::Expression::Str(str) => cst::Expression::Str(cst::Str { + chain: str + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..str + }), + cst::Expression::Num(num) => cst::Expression::Num(cst::Num { + chain: num + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..num + }), + cst::Expression::Bool(bool) => cst::Expression::Bool(cst::Bool { + chain: bool + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..bool + }), + cst::Expression::Null(null) => cst::Expression::Null(cst::Null { + chain: null + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..null + }), + cst::Expression::Obj(obj) => cst::Expression::Obj(cst::Obj { + value: obj + .value + .into_iter() + .map(|(key, expression)| Ok((key, self.visit_expression(expression)?))) + .collect::, AiScriptError>>()?, + chain: obj + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..obj + }), + cst::Expression::Arr(arr) => cst::Expression::Arr(cst::Arr { + value: arr + .value + .into_iter() + .map(|expression| self.visit_expression(expression)) + .collect::, AiScriptError>>()?, + chain: arr + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..arr + }), + cst::Expression::Identifier(identifier) => { + cst::Expression::Identifier(cst::Identifier { + chain: identifier + .chain + .map(|chain| { + chain + .into_iter() + .map(|chain_member| self.visit_chain_member(chain_member)) + .collect::, AiScriptError>>() + }) + .map_or(Ok(None), |r| r.map(Some))?, + ..identifier + }) + } + cst::Expression::Call(call) => cst::Expression::Call(cst::Call { + target: self.visit_expression(*call.target)?.into(), + args: call + .args + .into_iter() + .map(|expression| self.visit_expression(expression)) + .collect::, AiScriptError>>()?, + ..call + }), + cst::Expression::Index(index) => cst::Expression::Index(cst::Index { + target: self.visit_expression(*index.target)?.into(), + index: self.visit_expression(*index.index)?.into(), + ..index + }), + cst::Expression::Prop(prop) => cst::Expression::Prop(cst::Prop { + target: self.visit_expression(*prop.target)?.into(), + ..prop + }), + }) + } + + fn visit_chain_member( + &self, + chain_member: cst::ChainMember, + ) -> Result { + let chain_member = self.callback_chain_member(chain_member)?; + Ok(match chain_member { + cst::ChainMember::CallChain(call_chain) => { + cst::ChainMember::CallChain(cst::CallChain { + args: call_chain + .args + .into_iter() + .map(|expression| self.visit_expression(expression)) + .collect::, AiScriptError>>()?, + ..call_chain + }) + } + cst::ChainMember::IndexChain(index_chain) => { + cst::ChainMember::IndexChain(cst::IndexChain { + index: self.visit_expression(index_chain.index)?, + ..index_chain + }) + } + cst::ChainMember::PropChain(_) => chain_member, + }) + } + + fn callback_namespace( + &self, + namespace: cst::Namespace, + ) -> Result { + Ok(namespace) + } + + fn callback_meta(&self, meta: cst::Meta) -> Result { + Ok(meta) + } + + fn callback_statement( + &self, + statement: cst::Statement, + ) -> Result { + Ok(statement) + } + + fn callback_expression( + &self, + expression: cst::Expression, + ) -> Result { + Ok(expression) + } + + fn callback_chain_member( + &self, + chain_member: cst::ChainMember, + ) -> Result { + Ok(chain_member) + } +} diff --git a/src/type.rs b/src/type.rs new file mode 100644 index 0000000..ceccfdb --- /dev/null +++ b/src/type.rs @@ -0,0 +1,51 @@ +use crate::{error::AiScriptError, node as ast}; + +pub struct TSimple { + pub name: String, +} + +pub struct TGeneric { + pub name: String, + pub inners: Vec, +} + +pub struct TFn { + pub args: Vec, + pub result: Box, +} + +pub enum Type { + Simple(TSimple), + Generic(TGeneric), + Fn(TFn), +} + +pub fn get_type_by_source(type_source: ast::TypeSource) -> Result { + match type_source { + ast::TypeSource::NamedTypeSource(type_source) => match type_source.name.as_str() { + "null" | "bool" | "num" | "str" | "any" | "void" => Ok(Type::Simple(TSimple { + name: type_source.name, + })), + "arr" | "obj" => Ok(Type::Generic(TGeneric { + name: type_source.name, + inners: vec![type_source.inner.map_or_else( + || { + Ok(Type::Simple(TSimple { + name: "any".to_string(), + })) + }, + |inner| get_type_by_source(*inner), + )?], + })), + _ => todo!(), + }, + ast::TypeSource::FnTypeSource(type_source) => Ok(Type::Fn(TFn { + args: type_source + .args + .into_iter() + .map(get_type_by_source) + .collect::, AiScriptError>>()?, + result: get_type_by_source(*type_source.result)?.into(), + })), + } +} diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..2368eda --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,6091 @@ +use aiscript::{ + ast::*, + errors::{AiScriptError, AiScriptRuntimeError}, + utils, + values::Value, + Interpreter, Parser, +}; +use futures::FutureExt; +use indexmap::IndexMap; + +async fn test(program: &str, test: fn(Value)) -> Result { + let ast = Parser::default().parse(program)?; + let aiscript = Interpreter::new( + [], + None:: _>, + Some(move |value| { + test(value); + async move {}.boxed() + }), + None:: _>, + Some(9999), + ); + aiscript.exec(ast).await.map(|value| value.unwrap()) +} + +fn get_meta(program: &str) -> Result, Option>, AiScriptError> { + let ast = Parser::default().parse(program)?; + let metadata = Interpreter::collect_metadata(ast); + Ok(metadata) +} + +fn null() -> Value { + Value::null() +} + +fn bool(value: bool) -> Value { + Value::bool(value) +} + +fn num(value: impl Into) -> Value { + Value::num(value.into()) +} + +fn str(value: impl Into) -> Value { + Value::str(value.into()) +} + +fn arr(value: impl IntoIterator) -> Value { + Value::arr(value) +} + +fn obj(value: impl IntoIterator, Value)>) -> Value { + Value::obj(value) +} + +fn error(value: impl Into, info: Option) -> Value { + Value::error(value, info) +} + +#[tokio::test] +async fn hello_world() { + test("<: \"Hello, world!\"", |res| { + assert_eq!(res, str("Hello, world!")) + }) + .await + .unwrap(); +} + +#[test] +fn empty_script() { + let ast = Parser::default().parse("").unwrap(); + assert_eq!(ast, Vec::new()); +} + +mod interpreter { + use super::*; + + mod scope { + use super::*; + + #[tokio::test] + async fn get_all() { + let aiscript = Interpreter::default(); + aiscript + .exec( + Parser::default() + .parse( + " + let a = 1 + @b() { + let x = a + 1 + x + } + if true { + var y = 2 + } + var c = true + ", + ) + .unwrap(), + ) + .await + .unwrap(); + let vars = aiscript.scope.get_all(); + assert_ne!(vars.get("a"), None); + assert_ne!(vars.get("b"), None); + assert_ne!(vars.get("a"), None); + assert_eq!(vars.get("x"), None); + assert_eq!(vars.get("y"), None); + } + } +} + +mod ops { + use super::*; + + #[tokio::test] + async fn eq() { + test("<: (1 == 1)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (1 == 2)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + } + + #[tokio::test] + async fn neq() { + test("<: (1 != 2)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (1 != 1)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + } + + #[tokio::test] + async fn and() { + test("<: (true && true)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (true && false)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + test("<: (false && true)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + test("<: (false && false)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + test("<: (false && null)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + let err = test("<: (true && null)", |_| {}).await.unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + + test( + r#" + var tmp = null + + @func() { + tmp = true + return true + } + + false && func() + + <: tmp + "#, + |res| assert_eq!(res, null()), + ) + .await + .unwrap(); + + test( + r#" + var tmp = null + + @func() { + tmp = true + return true + } + + true && func() + + <: tmp + "#, + |res| assert_eq!(res, bool(true)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn or() { + test("<: (true || true)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (true || false)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (false || true)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (false || false)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + test("<: (true || null)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + let err = test("<: (false || null)", |_| {}).await.unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + + test( + r#" + var tmp = null + + @func() { + tmp = true + return true + } + + true || func() + + <: tmp + "#, + |res| assert_eq!(res, null()), + ) + .await + .unwrap(); + + test( + r#" + var tmp = null + + @func() { + tmp = true + return true + } + + false || func() + + <: tmp + "#, + |res| assert_eq!(res, bool(true)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn add() { + test("<: (1 + 1)", |res| assert_eq!(res, num(2))) + .await + .unwrap(); + } + + #[tokio::test] + async fn sub() { + test("<: (1 - 1)", |res| assert_eq!(res, num(0))) + .await + .unwrap(); + } + + #[tokio::test] + async fn mul() { + test("<: (1 * 1)", |res| assert_eq!(res, num(1))) + .await + .unwrap(); + } + + #[tokio::test] + async fn pow() { + test("<: (1 ^ 1)", |res| assert_eq!(res, num(1))) + .await + .unwrap(); + } + + #[tokio::test] + async fn div() { + test("<: (1 / 1)", |res| assert_eq!(res, num(1))) + .await + .unwrap(); + } + + #[tokio::test] + async fn mod_() { + test("<: (1 % 1)", |res| assert_eq!(res, num(0))) + .await + .unwrap(); + } + + #[tokio::test] + async fn gt() { + test("<: (2 > 1)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (1 > 1)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + test("<: (0 > 1)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + } + + #[tokio::test] + async fn lt() { + test("<: (2 < 1)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + test("<: (1 < 1)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + test("<: (0 < 1)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + } + + #[tokio::test] + async fn gteq() { + test("<: (2 >= 1)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (1 >= 1)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (0 >= 1)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + } + + #[tokio::test] + async fn lteq() { + test("<: (2 <= 1)", |res| assert_eq!(res, bool(false))) + .await + .unwrap(); + + test("<: (1 <= 1)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: (0 <= 1)", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + } + + #[tokio::test] + async fn precedence() { + test("<: 1 + 2 * 3 + 4", |res| assert_eq!(res, num(11))) + .await + .unwrap(); + + test("<: 1 + 4 / 4 + 1", |res| assert_eq!(res, num(3))) + .await + .unwrap(); + + test("<: 1 + 1 == 2 && 2 * 2 == 4", |res| { + assert_eq!(res, bool(true)) + }) + .await + .unwrap(); + + test("<: (1 + 1) * 2", |res| assert_eq!(res, num(4))) + .await + .unwrap(); + } + + #[tokio::test] + async fn negative_numbers() { + test("<: 1+-1", |res| assert_eq!(res, num(0))) + .await + .unwrap(); + + test("<: 1--1", |res| assert_eq!(res, num(2))) + .await + .unwrap(); + + test("<: -1*-1", |res| assert_eq!(res, num(1))) + .await + .unwrap(); + + test("<: -1==-1", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: 1>-1", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: -1<1", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + } +} + +mod infix_expression { + use super::*; + + #[tokio::test] + async fn simple_infix_expression() { + test("<: 0 < 1", |res| assert_eq!(res, bool(true))) + .await + .unwrap(); + + test("<: 1 + 1", |res| assert_eq!(res, num(2))) + .await + .unwrap(); + } + + #[tokio::test] + async fn combination() { + test("<: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10", |res| { + assert_eq!(res, num(55)) + }) + .await + .unwrap(); + + test("<: Core:add(1, 3) * Core:mul(2, 5)", |res| { + assert_eq!(res, num(40)) + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn use_parentheses_to_distinguish_expr() { + test("<: (1 + 10) * (2 + 5)", |res| assert_eq!(res, num(77))) + .await + .unwrap(); + } + + #[tokio::test] + async fn syntax_symbols_vs_infix_operators() { + test( + r#" + <: match true { + 1 == 1 => "true" + 1 < 1 => "false" + } + "#, + |res| assert_eq!(res, str("true")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn number_if_expression() { + test("<: 1 + if true 1 else 2", |res| assert_eq!(res, num(2))) + .await + .unwrap(); + } + + #[tokio::test] + async fn number_match_expression() { + test( + r#" + <: 1 + match 2 == 2 { + true => 3 + false => 4 + } + "#, + |res| assert_eq!(res, num(4)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn eval_eval() { + test("<: eval { 1 } + eval { 1 }", |res| assert_eq!(res, num(2))) + .await + .unwrap(); + } + + #[tokio::test] + async fn disallow_line_break() { + test( + r#" + <: 1 + + 1 + 1 + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn escaped_line_break() { + test( + r#" + <: 1 + \ + 1 + 1 + "#, + |res| assert_eq!(res, num(3)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn infix_to_fncall_on_namespace() { + test( + r#" + :: Hoge { + @add(x, y) { + x + y + } + } + <: Hoge:add(1, 2) + "#, + |res| assert_eq!(res, num(3)), + ) + .await + .unwrap(); + } +} + +mod comment { + use super::*; + + #[tokio::test] + async fn single_line_comment() { + test( + r#" + // let a = ... + let a = 42 + <: a + "#, + |res| assert_eq!(res, num(42)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn multi_line_comment() { + test( + r#" + /* variable declaration here... + let a = ... + */ + let a = 42 + <: a + "#, + |res| assert_eq!(res, num(42)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn multi_line_comment_2() { + test( + r#" + /* variable declaration here... + let a = ... + */ + let a = 42 + /* + another comment here + */ + <: a + "#, + |res| assert_eq!(res, num(42)), + ) + .await + .unwrap(); + } +} + +#[tokio::test] +async fn expression_containing_collon_is_not_object() { + test( + r#" + <: eval { + Core:eq("ai", "ai") + } + "#, + |res| assert_eq!(res, bool(true)), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn inc() { + test( + r#" + var a = 0 + a += 1 + a += 2 + a += 3 + <: a + "#, + |res| assert_eq!(res, num(6)), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn dec() { + test( + r#" + var a = 0 + a -= 1 + a -= 2 + a -= 3 + <: a + "#, + |res| assert_eq!(res, num(-6)), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn reference_is_not_chained() { + test( + r#" + var f = @() { "a" } + var g = f + f = @() { "b" } + + <: g() + "#, + |res| assert_eq!(res, str("a")), + ) + .await + .unwrap(); +} + +mod cannot_put_multiple_statements_in_a_line { + use super::*; + + #[tokio::test] + async fn var_def() { + test( + r#" + let a = 42 let b = 11 + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn var_def_op() { + test( + r#" + let a = 13 + 75 let b = 24 + 146 + "#, + |_| {}, + ) + .await + .unwrap_err(); + } +} + +#[tokio::test] +async fn empty_function() { + test( + r#" + @hoge() { } + <: hoge() + "#, + |res| assert_eq!(res, null()), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn empty_lambda() { + test( + r#" + let hoge = @() { } + <: hoge() + "#, + |res| assert_eq!(res, null()), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn lambda_that_returns_an_object() { + test( + r#" + let hoge = @() {{}} + <: hoge() + "#, + |res| assert_eq!(res, obj([] as [(String, Value); 0])), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn closure() { + test( + r#" + @store(v) { + let state = v + @() { + state + } + } + let s = store("ai") + <: s() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn closure_counter() { + test( + r#" + @create_counter() { + var count = 0 + { + get_count: @() { count }; + count: @() { count = (count + 1) }; + } + } + + let counter = create_counter() + let get_count = counter.get_count + let count = counter.count + + count() + count() + count() + + <: get_count() + "#, + |res| assert_eq!(res, num(3)), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn recursion() { + test( + r#" + @fact(n) { + if (n == 0) { 1 } else { (fact((n - 1)) * n) } + } + + <: fact(5) + "#, + |res| assert_eq!(res, num(120)), + ) + .await + .unwrap(); +} + +mod var_name_starts_with_reserved_word { + use super::*; + + #[tokio::test] + async fn let_() { + test( + r#" + @f() { + let letcat = "ai" + letcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn var() { + test( + r#" + @f() { + let varcat = "ai" + varcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn return_() { + test( + r#" + @f() { + let returncat = "ai" + returncat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn each() { + test( + r#" + @f() { + let eachcat = "ai" + eachcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn for_() { + test( + r#" + @f() { + let forcat = "ai" + forcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn loop_() { + test( + r#" + @f() { + let loopcat = "ai" + loopcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn break_() { + test( + r#" + @f() { + let breakcat = "ai" + breakcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn continue_() { + test( + r#" + @f() { + let continuecat = "ai" + continuecat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn if_() { + test( + r#" + @f() { + let ifcat = "ai" + ifcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn match_() { + test( + r#" + @f() { + let matchcat = "ai" + matchcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn true_() { + test( + r#" + @f() { + let truecat = "ai" + truecat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn false_() { + test( + r#" + @f() { + let falsecat = "ai" + falsecat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn null() { + test( + r#" + @f() { + let nullcat = "ai" + nullcat + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } +} + +mod name_validation_of_reserved_word { + use super::*; + + #[tokio::test] + async fn def() { + test( + r#" + let let = 1 + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn attr() { + test( + r#" + #[let 1] + @f() { 1 } + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn ns() { + test( + r#" + :: let { + @f() { 1 } + } + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn var() { + test( + r#" + let + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn prop() { + test( + r#" + let x = { let: 1 } + x.let + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn meta() { + test( + r#" + ### let 1 + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn fn_() { + test( + r#" + @let() { 1 } + "#, + |_| {}, + ) + .await + .unwrap_err(); + } +} + +mod object { + use super::*; + + #[tokio::test] + async fn property_access() { + test( + r#" + let obj = { + a: { + b: { + c: 42; + }; + }; + } + + <: obj.a.b.c + "#, + |res| assert_eq!(res, num(42)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn property_access_fn_call() { + test( + r#" + @f() { 42 } + + let obj = { + a: { + b: { + c: f; + }; + }; + } + + <: obj.a.b.c() + "#, + |res| assert_eq!(res, num(42)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn property_assign() { + test( + r#" + let obj = { + a: 1 + b: { + c: 2 + d: { + e: 3 + } + } + } + + obj.a = 24 + obj.b.d.e = 42 + + <: obj + "#, + |res| { + assert_eq!( + res, + obj([ + ("a", num(24)), + ("b", obj([("c", num(2)), ("d", obj([("e", num(42))]))])) + ]) + ) + }, + ) + .await + .unwrap(); + } +} + +mod array { + + use super::*; + + #[tokio::test] + async fn array_item_access() { + test( + r#" + let arr = ["ai", "chan", "kawaii"] + + <: arr[1] + "#, + |res| assert_eq!(res, str("chan")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn array_item_assign() { + test( + r#" + let arr = ["ai", "chan", "kawaii"] + + arr[1] = "taso" + + <: arr + "#, + |res| assert_eq!(res, arr([str("ai"), str("taso"), str("kawaii"),])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn assign_array_item_to_out_of_range() { + let err = test( + r#" + let arr = [1, 2, 3] + + arr[3] = 4 + + <: null + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!( + err, + AiScriptError::Runtime(AiScriptRuntimeError::IndexOutOfRange(_, _)) + )); + + let err = test( + r#" + let arr = [1, 2, 3] + + arr[9] = 10 + + <: null + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!( + err, + AiScriptError::Runtime(AiScriptRuntimeError::IndexOutOfRange(_, _)) + )); + } + + #[tokio::test] + async fn index_out_of_range_error() { + let err = test( + r#" + <: [42][1] + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!( + err, + AiScriptError::Runtime(AiScriptRuntimeError::IndexOutOfRange(_, _)) + )); + } + + #[tokio::test] + async fn index_out_of_range_on_assignment() { + let err = test( + r#" + var a = [] + a[2] = 'hoge' + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!( + err, + AiScriptError::Runtime(AiScriptRuntimeError::IndexOutOfRange(_, _)) + )); + } + + #[tokio::test] + async fn non_integer_indexed_assignment() { + let err = test( + r#" + var a = [] + a[6.21] = 'hoge' + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!( + err, + AiScriptError::Runtime(AiScriptRuntimeError::IndexOutOfRange(_, _)) + )); + } +} + +mod chain { + use super::*; + + #[tokio::test] + async fn chain_access_prop_index_call() { + test( + r#" + let obj = { + a: { + b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }]; + }; + } + + <: obj.a.b[0]("ai") + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn chained_assign_left_side_prop_index() { + test( + r#" + let obj = { + a: { + b: ["ai", "chan", "kawaii"]; + }; + } + + obj.a.b[1] = "taso" + + <: obj + "#, + |res| { + assert_eq!( + res, + obj([( + "a", + obj([("b", arr([str("ai"), str("taso"), str("kawaii"),]))]) + )]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn chained_assign_right_side_prop_index_call() { + test( + r#" + let obj = { + a: { + b: ["ai", "chan", "kawaii"]; + }; + } + + var x = null + x = obj.a.b[1] + + <: x + "#, + |res| assert_eq!(res, str("chan"),), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn chained_inc_dec_left_side_index_prop() { + test( + r#" + let arr = [ + { + a: 1; + b: 2; + } + ] + + arr[0].a += 1 + arr[0].b -= 1 + + <: arr + "#, + |res| assert_eq!(res, arr([obj([("a", num(2)), ("b", num(1)),])])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn chained_inc_dec_left_side_prop_index() { + test( + r#" + let obj = { + a: { + b: [1, 2, 3]; + }; + } + + obj.a.b[1] += 1 + obj.a.b[2] -= 1 + + <: obj + "#, + |res| { + assert_eq!( + res, + obj([("a", obj([("b", arr([num(1), num(3), num(2),]))]))]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn prop_in_def() { + test( + r#" + let x = @() { + let obj = { + a: 1 + } + obj.a + } + + <: x() + "#, + |res| assert_eq!(res, num(1)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn prop_in_return() { + test( + r#" + let x = @() { + let obj = { + a: 1 + } + return obj.a + 2 + } + + <: x() + "#, + |res| assert_eq!(res, num(1)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn prop_in_each() { + test( + r#" + let msgs = [] + let x = { a: ["ai", "chan", "kawaii"] } + each let item, x.a { + let y = { a: item } + msgs.push([y.a, "!"].join()) + } + <: msgs + "#, + |res| assert_eq!(res, arr([str("ai!"), str("chan!"), str("kawaii!"),])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn prop_in_for() { + test( + r#" + let x = { times: 10, count: 0 } + for (let i, x.times) { + x.count = (x.count + i) + } + <: x.count + "#, + |res| assert_eq!(res, num(45)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn object_with_index() { + test( + r#" + let ai = {a: {}}['a'] + ai['chan'] = 'kawaii' + <: ai[{a: 'chan'}['a']] + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn property_chain_with_parenthesis() { + let ast = Parser::default() + .parse( + r#" + (a.b).c + "#, + ) + .unwrap(); + let line = ast.first().unwrap().clone(); + if let Node::Expression(Expression::Prop(Prop { target, name, .. })) = line { + assert_eq!(name, "c".to_string()); + if let Expression::Prop(Prop { target, name, .. }) = *target { + assert_eq!(name, "b".to_string()); + if let Expression::Identifier(Identifier { name, .. }) = *target { + assert_eq!(name, "a".to_string()); + return; + } + } + } + panic!(); + } + + #[tokio::test] + async fn index_chain_with_parenthesis() { + let ast = Parser::default() + .parse( + r#" + (a[42]).b + "#, + ) + .unwrap(); + let line = ast.first().unwrap().clone(); + if let Node::Expression(Expression::Prop(Prop { target, name, .. })) = line { + assert_eq!(name, "b".to_string()); + if let Expression::Index(Index { target, index, .. }) = *target { + if let ( + Expression::Identifier(Identifier { name, .. }), + Expression::Num(Num { value, .. }), + ) = (*target, *index) + { + assert_eq!(name, "a".to_string()); + assert_eq!(value, 42.0); + return; + } + } + } + panic!(); + } + + #[tokio::test] + async fn call_chain_with_parenthesis() { + let ast = Parser::default() + .parse( + r#" + (foo(42, 57)).bar + "#, + ) + .unwrap(); + let line = ast.first().unwrap().clone(); + if let Node::Expression(Expression::Prop(Prop { target, name, .. })) = line { + assert_eq!(name, "bar".to_string()); + if let Expression::Call(Call { target, args, .. }) = *target { + if let Expression::Identifier(Identifier { name, .. }) = *target { + assert_eq!(name, "foo".to_string()); + if let [Expression::Num(Num { value: arg1, .. }), Expression::Num(Num { value: arg2, .. })] = + args[..] + { + assert_eq!(arg1, 42.0); + assert_eq!(arg2, 57.0); + return; + } + } + } + } + panic!(); + } + + #[tokio::test] + async fn longer_chain_with_parenthesis() { + let ast = Parser::default() + .parse( + r#" + (a.b.c).d.e + "#, + ) + .unwrap(); + let line = ast.first().unwrap().clone(); + if let Node::Expression(Expression::Prop(Prop { target, name, .. })) = line { + assert_eq!(name, "e".to_string()); + if let Expression::Prop(Prop { target, name, .. }) = *target { + assert_eq!(name, "d".to_string()); + if let Expression::Prop(Prop { target, name, .. }) = *target { + assert_eq!(name, "c".to_string()); + if let Expression::Prop(Prop { target, name, .. }) = *target { + assert_eq!(name, "b".to_string()); + if let Expression::Identifier(Identifier { name, .. }) = *target { + assert_eq!(name, "a".to_string()); + return; + } + } + } + } + } + panic!(); + } +} + +mod template_syntax { + use super::*; + + #[tokio::test] + async fn basic() { + test( + r#" + let str = "kawaii" + <: `Ai is {str}!` + "#, + |res| assert_eq!(res, str("Ai is kawaii!")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn convert_to_str() { + test( + r#" + <: `1 + 1 = {(1 + 1)}` + "#, + |res| assert_eq!(res, str("1 + 1 = 2")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn invalid() { + test( + r#" + <: `{hoge}` + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn escape() { + test( + r#" + let message = "Hello" + <: `\`a\{b\}c\`` + "#, + |res| assert_eq!(res, str("`a{b}c`")), + ) + .await + .unwrap(); + } +} + +#[tokio::test] +async fn throws_error_when_divided_by_zero() { + test( + r#" + <: (0 / 0) + "#, + |_| {}, + ) + .await + .unwrap_err(); +} + +mod function_call { + use super::*; + + #[tokio::test] + async fn without_args() { + test( + r#" + @f() { + 42 + } + <: f() + "#, + |res| assert_eq!(res, num(42)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn with_args() { + test( + r#" + @f(x) { + x + } + <: f(42) + "#, + |res| assert_eq!(res, num(42)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn with_args_separated_by_comma() { + test( + r#" + @f(x, y) { + (x + y) + } + <: f(1, 1) + "#, + |res| assert_eq!(res, num(2)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn with_args_separated_by_space() { + test( + r#" + @f(x y) { + (x + y) + } + <: f(1 1) + "#, + |res| assert_eq!(res, num(2)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn std_throw_aiscript_error_when_required_arg_missing() { + let err = test( + r#" + <: Core:eq(1) + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + } + + #[tokio::test] + async fn omitted_args() { + test( + r#" + @f(x, y) { + [x, y] + } + <: f(1) + "#, + |res| assert_eq!(res, arr([num(1), null()])), + ) + .await + .unwrap(); + } +} + +mod return_ { + use super::*; + + #[tokio::test] + async fn early_return() { + test( + r#" + @f() { + if true { + return "ai" + } + + "pope" + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn early_return_nested() { + test( + r#" + @f() { + if true { + if true { + return "ai" + } + } + + "pope" + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn early_return_nested_2() { + test( + r#" + @f() { + if true { + return "ai" + } + + "pope" + } + + @g() { + if (f() == "ai") { + return "kawaii" + } + + "pope" + } + + <: g() + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn early_return_without_block() { + test( + r#" + @f() { + if true return "ai" + + "pope" + } + <: f() + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn return_inside_for() { + test( + r#" + @f() { + var count = 0 + for (let i, 100) { + count += 1 + if (i == 42) { + return count + } + } + } + <: f() + "#, + |res| assert_eq!(res, num(43)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn return_inside_for_2() { + test( + r#" + @f() { + for (let i, 10) { + return 1 + } + 2 + } + <: f() + "#, + |res| assert_eq!(res, num(1)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn return_inside_loop() { + test( + r#" + @f() { + var count = 0 + loop { + count += 1 + if (count == 42) { + return count + } + } + } + <: f() + "#, + |res| assert_eq!(res, num(42)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn return_inside_loop_2() { + test( + r#" + @f() { + loop { + return 1 + } + 2 + } + <: f() + "#, + |res| assert_eq!(res, num(1)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn return_inside_each() { + test( + r#" + @f() { + var count = 0 + each (let item, ["ai", "chan", "kawaii"]) { + count += 1 + if (item == "chan") { + return count + } + } + } + <: f() + "#, + |res| assert_eq!(res, num(2)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn return_inside_each_2() { + test( + r#" + @f() { + each (let item, ["ai", "chan", "kawaii"]) { + return 1 + } + 2 + } + <: f() + "#, + |res| assert_eq!(res, num(1)), + ) + .await + .unwrap(); + } +} + +mod eval { + use super::*; + + #[tokio::test] + async fn returns_value() { + test( + r#" + let foo = eval { + let a = 1 + let b = 2 + (a + b) + } + + <: foo + "#, + |res| assert_eq!(res, num(3)), + ) + .await + .unwrap(); + } +} + +mod exists { + use super::*; + + #[tokio::test] + async fn basic() { + test( + r#" + let foo = null + <: [(exists foo) (exists bar)] + "#, + |res| assert_eq!(res, arr([bool(true), bool(false)])), + ) + .await + .unwrap(); + } +} + +mod if_ { + use super::*; + + #[tokio::test] + async fn if_() { + test( + r#" + var msg = "ai" + if true { + msg = "kawaii" + } + <: msg + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + + test( + r#" + var msg = "ai" + if false { + msg = "kawaii" + } + <: msg + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn else_() { + test( + r#" + var msg = null + if true { + msg = "ai" + } else { + msg = "kawaii" + } + <: msg + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + + test( + r#" + var msg = null + if false { + msg = "ai" + } else { + msg = "kawaii" + } + <: msg + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn elif() { + test( + r#" + var msg = "bebeyo" + if false { + msg = "ai" + } elif true { + msg = "kawaii" + } + <: msg + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + + test( + r#" + var msg = "bebeyo" + if false { + msg = "ai" + } elif false { + msg = "kawaii" + } + <: msg + "#, + |res| assert_eq!(res, str("bebeyo")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn if_elif_else() { + test( + r#" + var msg = null + if false { + msg = "ai" + } elif true { + msg = "chan" + } else { + msg = "kawaii" + } + <: msg + "#, + |res| assert_eq!(res, str("chan")), + ) + .await + .unwrap(); + + test( + r#" + var msg = null + if false { + msg = "ai" + } elif false { + msg = "chan" + } else { + msg = "kawaii" + } + <: msg + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn expr() { + test( + r#" + <: if true "ai" else "kawaii" + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + + test( + r#" + <: if false "ai" else "kawaii" + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + } +} + +mod match_ { + use super::*; + + #[tokio::test] + async fn basic() { + test( + r#" + <: match 2 { + 1 => "a" + 2 => "b" + 3 => "c" + } + "#, + |res| assert_eq!(res, str("b")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn when_default_not_provided_returns_null() { + test( + r#" + <: match 42 { + 1 => "a" + 2 => "b" + 3 => "c" + } + "#, + |res| assert_eq!(res, null()), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn with_default() { + test( + r#" + <: match 42 { + 1 => "a" + 2 => "b" + 3 => "c" + * => "d" + } + "#, + |res| assert_eq!(res, str("d")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn with_block() { + test( + r#" + <: match 2 { + 1 => 1 + 2 => { + let a = 1 + let b = 2 + (a + b) + } + 3 => 3 + } + "#, + |res| assert_eq!(res, num(3)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn with_return() { + test( + r#" + @f(x) { + match x { + 1 => { + return "ai" + } + } + "foo" + } + <: f(1) + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } +} + +mod loop_ { + use super::*; + + #[tokio::test] + async fn basic() { + test( + r#" + var count = 0 + loop { + if (count == 10) break + count = (count + 1) + } + <: count + "#, + |res| assert_eq!(res, num(10)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn with_continue() { + test( + r#" + var a = ["ai" "chan" "kawaii" "yo" "!"] + var b = [] + loop { + var x = a.shift() + if (x == "chan") continue + if (x == "yo") break + b.push(x) + } + <: b + "#, + |res| assert_eq!(res, arr([str("ai"), str("kawaii")])), + ) + .await + .unwrap(); + } +} + +mod for_ { + use super::*; + + #[tokio::test] + async fn basic() { + test( + r#" + var count = 0 + for (let i, 10) { + count += i + 1 + } + <: count + "#, + |res| assert_eq!(res, num(55)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn initial_value() { + test( + r#" + var count = 0 + for (let i = 2, 10) { + count += i + } + <: count + "#, + |res| assert_eq!(res, num(65)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn without_iterator() { + test( + r#" + var count = 0 + for (10) { + count = (count + 1) + } + <: count + "#, + |res| assert_eq!(res, num(10)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn without_brackets() { + test( + r#" + var count = 0 + for let i, 10 { + count = (count + i) + } + <: count + "#, + |res| assert_eq!(res, num(45)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn break_() { + test( + r#" + var count = 0 + for (let i, 20) { + if (i == 11) break + count += i + } + <: count + "#, + |res| assert_eq!(res, num(55)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn continue_() { + test( + r#" + var count = 0 + for (let i, 10) { + if (i == 5) continue + count = (count + 1) + } + <: count + "#, + |res| assert_eq!(res, num(9)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn single_statement() { + test( + r#" + var count = 0 + for 10 count += 1 + <: count + "#, + |res| assert_eq!(res, num(10)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn var_name_without_space() { + test( + r#" + for (leti, 10) { + <: i + } + "#, + |_| {}, + ) + .await + .unwrap_err(); + } +} + +mod for_of { + use super::*; + + #[tokio::test] + async fn standard() { + test( + r#" + let msgs = [] + each let item, ["ai", "chan", "kawaii"] { + msgs.push([item, "!"].join()) + } + <: msgs + "#, + |res| assert_eq!(res, arr([str("ai!"), str("chan!"), str("kawaii!")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn break_() { + test( + r#" + let msgs = [] + each let item, ["ai", "chan", "kawaii" "yo"] { + if (item == "kawaii") break + msgs.push([item, "!"].join()) + } + <: msgs + "#, + |res| assert_eq!(res, arr([str("ai!"), str("chan!")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn single_statement() { + test( + r#" + let msgs = [] + each let item, ["ai", "chan", "kawaii"] msgs.push([item, "!"].join()) + <: msgs + "#, + |res| assert_eq!(res, arr([str("ai!"), str("chan!"), str("kawaii!")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn var_name_without_space() { + test( + r#" + each letitem, ["ai", "chan", "kawaii"] { + <: item + } + "#, + |_| {}, + ) + .await + .unwrap_err(); + } +} + +mod not { + use super::*; + + #[tokio::test] + async fn basic() { + test( + r#" + <: !true + "#, + |res| assert_eq!(res, bool(false)), + ) + .await + .unwrap(); + } +} + +mod namespace { + use super::*; + + #[tokio::test] + async fn standard() { + test( + r#" + <: Foo:bar() + + :: Foo { + @bar() { "ai" } + } + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn self_ref() { + test( + r#" + <: Foo:bar() + + :: Foo { + let ai = "kawaii" + @bar() { ai } + } + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn cannot_declare_mutable_variable() { + test( + r#" + :: Foo { + var ai = "kawaii" + } + "#, + |_| {}, + ) + .await + .unwrap_err(); + } + + #[tokio::test] + async fn nested() { + test( + r#" + <: Foo:Bar:baz() + + :: Foo { + :: Bar { + @baz() { "ai" } + } + } + "#, + |res| assert_eq!(res, str("ai")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn nested_ref() { + test( + r#" + <: Foo:baz + + :: Foo { + let baz = Bar:ai + :: Bar { + let ai = "kawaii" + } + } + "#, + |res| assert_eq!(res, str("kawaii")), + ) + .await + .unwrap(); + } +} + +mod literal { + use super::*; + + #[tokio::test] + async fn string_single_quote() { + test( + r#" + <: 'foo' + "#, + |res| assert_eq!(res, str("foo")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn string_double_quote() { + test( + r#" + <: "foo" + "#, + |res| assert_eq!(res, str("foo")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn escaped_double_quote() { + test( + r#" + <: "ai saw a note \"bebeyo\"." + "#, + |res| assert_eq!(res, str(r#"ai saw a note "bebeyo"."#)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn escaped_single_quote() { + test( + r#" + <: 'ai saw a note \'bebeyo\'.' + "#, + |res| assert_eq!(res, str("ai saw a note 'bebeyo'.")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn bool_true() { + test( + r#" + <: true + "#, + |res| assert_eq!(res, bool(true)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn bool_false() { + test( + r#" + <: false + "#, + |res| assert_eq!(res, bool(false)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn number_int() { + test( + r#" + <: 10 + "#, + |res| assert_eq!(res, num(10)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn number_float() { + test( + r#" + <: 0.5 + "#, + |res| assert_eq!(res, num(0.5)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn arr_separated_by_comma() { + test( + r#" + <: [1, 2, 3] + "#, + |res| assert_eq!(res, arr([num(1), num(2), num(3)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn arr_separated_by_comma_with_trailing_comma() { + test( + r#" + <: [1, 2, 3,] + "#, + |res| assert_eq!(res, arr([num(1), num(2), num(3)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn arr_separated_by_line_break() { + test( + r#" + <: [ + 1 + 2 + 3 + ] + "#, + |res| assert_eq!(res, arr([num(1), num(2), num(3)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn arr_separated_by_line_break_and_comma() { + test( + r#" + <: [ + 1, + 2, + 3 + ] + "#, + |res| assert_eq!(res, arr([num(1), num(2), num(3)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn arr_separated_by_line_break_and_comma_with_trailing_comma() { + test( + r#" + <: [ + 1, + 2, + 3, + ] + "#, + |res| assert_eq!(res, arr([num(1), num(2), num(3)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn obj_separated_by_comma() { + test( + r#" + <: { a: 1, b: 2, c: 3 } + "#, + |res| assert_eq!(res, obj([("a", num(1)), ("b", num(2)), ("c", num(3))])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn obj_separated_by_comma_with_trailing_comma() { + test( + r#" + <: { a: 1, b: 2, c: 3, } + "#, + |res| assert_eq!(res, obj([("a", num(1)), ("b", num(2)), ("c", num(3))])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn obj_separated_by_semicolon() { + test( + r#" + <: { a: 1; b: 2; c: 3 } + "#, + |res| assert_eq!(res, obj([("a", num(1)), ("b", num(2)), ("c", num(3))])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn obj_separated_by_semicolon_with_trailing_semicolon() { + test( + r#" + <: { a: 1; b: 2; c: 3; } + "#, + |res| assert_eq!(res, obj([("a", num(1)), ("b", num(2)), ("c", num(3))])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn obj_separated_by_line_break() { + test( + r#" + <: { + a: 1 + b: 2 + c: 3 + } + "#, + |res| assert_eq!(res, obj([("a", num(1)), ("b", num(2)), ("c", num(3))])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn obj_separated_by_line_break_and_semicolon() { + test( + r#" + <: { + a: 1; + b: 2; + c: 3 + } + "#, + |res| assert_eq!(res, obj([("a", num(1)), ("b", num(2)), ("c", num(3))])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn obj_separated_by_line_break_and_semicolon_with_trailing_semicolon() { + test( + r#" + <: { + a: 1; + b: 2; + c: 3; + } + "#, + |res| assert_eq!(res, obj([("a", num(1)), ("b", num(2)), ("c", num(3))])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn obj_and_arr_separated_by_line_break() { + test( + r#" + <: { + a: 1 + b: [ + 1 + 2 + 3 + ] + c: 3 + } + "#, + |res| { + assert_eq!( + res, + obj([ + ("a", num(1)), + ("b", arr([num(1), num(2), num(3)])), + ("c", num(3)) + ]) + ) + }, + ) + .await + .unwrap(); + } +} + +mod type_declaration { + use super::*; + + #[tokio::test] + async fn def() { + test( + r#" + let abc: num = 1 + var xyz: str = "abc" + <: [abc xyz] + "#, + |res| assert_eq!(res, arr([num(1), str("abc")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn fn_def() { + test( + r#" + @f(x: arr, y: str, z: @(num) => bool): arr { + x.push(0) + y = "abc" + var r: bool = z(x[0]) + x.push(if r 5 else 10) + x + } + + <: f([1, 2, 3], "a", @(n) { n == 1 }) + "#, + |res| assert_eq!(res, arr([num(1), num(2), num(3), num(0), num(5)])), + ) + .await + .unwrap(); + } +} + +mod meta { + use super::*; + + #[test] + fn default_meta() { + let res = get_meta( + r#" + ### { a: 1; b: 2; c: 3; } + "#, + ) + .unwrap(); + assert_eq!( + res, + IndexMap::, Option>::from_iter([( + None, + Some(obj([("a", (num(1))), ("b", (num(2))), ("c", (num(3)))])) + )]) + ); + assert_eq!( + res.get(&None).cloned(), + Some(Some(obj([ + ("a", (num(1))), + ("b", (num(2))), + ("c", (num(3))) + ]))) + ) + } + + mod string { + use super::*; + + #[test] + fn valid() { + let res = get_meta( + r#" + ### x "hoge" + "#, + ) + .unwrap(); + assert_eq!( + res, + IndexMap::, Option>::from_iter([( + Some("x".to_string()), + Some(str("hoge")) + )]) + ); + } + } + + mod number { + use super::*; + + #[test] + fn valid() { + let res = get_meta( + r#" + ### x 42 + "#, + ) + .unwrap(); + assert_eq!( + res, + IndexMap::, Option>::from_iter([( + Some("x".to_string()), + Some(num(42)) + )]) + ); + } + } + + mod boolean { + use super::*; + + #[test] + fn valid() { + let res = get_meta( + r#" + ### x true + "#, + ) + .unwrap(); + assert_eq!( + res, + IndexMap::, Option>::from_iter([( + Some("x".to_string()), + Some(bool(true)) + )]) + ); + } + } + + mod null { + use super::*; + + #[test] + fn valid() { + let res = get_meta( + r#" + ### x null + "#, + ) + .unwrap(); + assert_eq!( + res, + IndexMap::, Option>::from_iter([( + Some("x".to_string()), + Some(null()) + )]) + ); + } + } + + mod array { + use super::*; + + #[test] + fn valid() { + let res = get_meta( + r#" + ### x [1 2 3] + "#, + ) + .unwrap(); + assert_eq!( + res, + IndexMap::, Option>::from_iter([( + Some("x".to_string()), + Some(arr([num(1), num(2), num(3)])) + )]) + ); + } + + #[test] + fn invalid() { + get_meta( + r#" + ### x [1 (2 + 2) 3] + "#, + ) + .unwrap_err(); + } + } + + mod object { + use super::*; + + #[test] + fn valid() { + let res = get_meta( + r#" + ### x { a: 1; b: 2; c: 3; } + "#, + ) + .unwrap(); + assert_eq!( + res, + IndexMap::, Option>::from_iter([( + Some("x".to_string()), + Some(obj([("a", num(1)), ("b", num(2)), ("c", num(3))])) + )]) + ); + } + + #[test] + fn invalid() { + get_meta( + r#" + ### x { a: 1; b: (2 + 2); c: 3; } + "#, + ) + .unwrap_err(); + } + } + + mod template { + use super::*; + + #[test] + fn invalid() { + get_meta( + r#" + ### x `foo {bar} baz` + "#, + ) + .unwrap_err(); + } + } + + mod expression { + use super::*; + + #[test] + fn invalid() { + get_meta( + r#" + ### x (1 + 1) + "#, + ) + .unwrap_err(); + } + } +} + +mod lang_version { + use super::*; + + #[test] + fn number() { + let res = utils::get_lang_version( + r#" + /// @2021 + @f(x) { + x + } + "#, + ); + assert_eq!(res, Some("2021".to_string())); + } + + #[test] + fn chars() { + let res = utils::get_lang_version( + r#" + /// @ canary + const a = 1 + @f(x) { + x + } + f(a) + "#, + ); + assert_eq!(res, Some("canary".to_string())); + } + + #[test] + fn complex() { + let res = utils::get_lang_version( + r#" + /// @ 2.0-Alpha + @f(x) { + x + } + "#, + ); + assert_eq!(res, Some("2.0-Alpha".to_string())); + } + + #[test] + fn no_specified() { + let res = utils::get_lang_version( + r#" + @f(x) { + x + } + "#, + ); + assert_eq!(res, None); + } +} + +mod attribute { + use super::*; + + #[test] + fn single_attribute_with_function_str() { + let nodes = Parser::default() + .parse( + r#" + #[Event "Received"] + @onReceived(data) { + data + } + "#, + ) + .unwrap(); + if let [Node::Statement(Statement::Definition(Definition { name, attr, .. }))] = &nodes[..] + { + assert_eq!(name, "onReceived"); + if let Some(attr) = attr { + if let [Attribute { + name, + value: Expression::Str(Str { value, .. }), + .. + }] = &attr[..] + { + assert_eq!(name, "Event"); + assert_eq!(value, "Received"); + return; + } + } + } + panic!(); + } + + #[test] + fn multiple_attributes_with_function_obj_str_bool() { + let nodes = Parser::default() + .parse( + r#" + #[Endpoint { path: "/notes/create"; }] + #[Desc "Create a note."] + #[Cat true] + @createNote(text) { + <: text + } + "#, + ) + .unwrap(); + if let [Node::Statement(Statement::Definition(Definition { name, attr, .. }))] = &nodes[..] + { + assert_eq!(name, "createNote"); + if let Some(attr) = attr { + if let [Attribute { + name: name1, + value: Expression::Obj(Obj { value: value1, .. }), + .. + }, Attribute { + name: name2, + value: Expression::Str(Str { value: value2, .. }), + .. + }, Attribute { + name: name3, + value: Expression::Bool(Bool { value: true, .. }), + .. + }] = &attr[..] + { + assert_eq!(name1, "Endpoint"); + assert_eq!(name2, "Desc"); + assert_eq!(value2, "Create a note."); + assert_eq!(name3, "Cat"); + if let [(key, Expression::Str(Str { value, .. }))] = + value1.iter().collect::>()[..] + { + assert_eq!(key, "path"); + assert_eq!(value, "/notes/create"); + return; + } + } + } + } + panic!(); + } + + #[test] + fn single_attribute_no_value() { + let nodes = Parser::default() + .parse( + r#" + #[serializable] + let data = 1 + "#, + ) + .unwrap(); + if let [Node::Statement(Statement::Definition(Definition { name, attr, .. }))] = &nodes[..] + { + assert_eq!(name, "data"); + if let Some(attr) = attr { + if let [Attribute { + name, + value: Expression::Bool { .. }, + .. + }] = &attr[..] + { + assert_eq!(name, "serializable"); + return; + } + } + } + panic!(); + } +} + +mod location { + use super::*; + + #[test] + fn function() { + let nodes = Parser::default() + .parse( + r#" + @f(a) { a } + "#, + ) + .unwrap(); + if let [Node::Statement(Statement::Definition(Definition { + loc: Some(Loc { start, end }), + .. + }))] = &nodes[..] + { + assert_eq!(start.clone(), 3); + assert_eq!(end.clone(), 13); + return; + } + panic!(); + } + + #[test] + fn comment() { + let nodes = Parser::default() + .parse( + r#" + /* + */ + // hoge + @f(a) { a } + "#, + ) + .unwrap(); + if let [Node::Statement(Statement::Definition(Definition { + loc: Some(Loc { start, end }), + .. + }))] = &nodes[..] + { + assert_eq!(start.clone(), 23); + assert_eq!(end.clone(), 33); + return; + } + panic!(); + } +} + +mod variable_declaration { + use super::*; + + #[tokio::test] + async fn do_not_assign_to_let_issue_328() { + let err = test( + r#" + let hoge = 33 + hoge = 4 + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + } +} + +mod variable_assignment { + use super::*; + + #[tokio::test] + async fn simple() { + test( + r#" + var hoge = 25 + hoge = 7 + <: hoge + "#, + |res| assert_eq!(res, num(7)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn destructuring_assingment() { + test( + r#" + var hoge = 'foo' + var fuga = { value: 'bar' } + [{ value: hoge }, fuga] = [fuga, hoge] + <: [hoge, fuga] + "#, + |res| assert_eq!(res, arr([str("bar"), str("foo")])), + ) + .await + .unwrap(); + } +} + +mod primitive_props { + use super::*; + + mod num { + use super::*; + + #[tokio::test] + async fn to_str() { + test( + r#" + let num = 123 + <: num.to_str() + "#, + |res| assert_eq!(res, str("123")), + ) + .await + .unwrap(); + } + } + + mod str { + use super::*; + + #[tokio::test] + async fn len() { + test( + r#" + let str = "hello" + <: str.len + "#, + |res| assert_eq!(res, num(5)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_num() { + test( + r#" + let str = "123" + <: str.to_num() + "#, + |res| assert_eq!(res, num(123)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn upper() { + test( + r#" + let str = "hello" + <: str.upper() + "#, + |res| assert_eq!(res, str("HELLO")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn lower() { + test( + r#" + let str = "HELLO" + <: str.lower() + "#, + |res| assert_eq!(res, str("hello")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn trim() { + test( + r#" + let str = " hello " + <: str.trim() + "#, + |res| assert_eq!(res, str("hello")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn replace() { + test( + r#" + let str = "hello" + <: str.replace("l", "x") + "#, + |res| assert_eq!(res, str("hexxo")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn index_of() { + test( + r#" + let str = '0123401234' + <: [ + str.index_of('3') == 3, + str.index_of('5') == -1, + str.index_of('3', 3) == 3, + str.index_of('3', 4) == 8, + str.index_of('3', -1) == -1, + str.index_of('3', -2) == 8, + str.index_of('3', -7) == 3, + str.index_of('3', 10) == -1, + ].map(@(v){if (v) '1' else '0'}).join() + "#, + |res| assert_eq!(res, str("11111111")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn incl() { + test( + r#" + let str = "hello" + <: [str.incl("ll"), str.incl("x")] + "#, + |res| assert_eq!(res, arr([bool(true), bool(false)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn split() { + test( + r#" + let str = "a,b,c" + <: str.split(",") + "#, + |res| assert_eq!(res, arr([str("a"), str("b"), str("c")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn pick() { + test( + r#" + let str = "hello" + <: str.pick(1) + "#, + |res| assert_eq!(res, str("e")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn slice() { + test( + r#" + let str = "hello" + <: str.slice(1, 3) + "#, + |res| assert_eq!(res, str("el")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn slice_out_of_range() { + test( + r#" + let str = "hello" + <: str.slice(3, 1) + "#, + |res| assert_eq!(res, str("")), + ) + .await + .unwrap(); + + test( + r#" + let str = "hello" + <: str.slice(-1, 3) + "#, + |res| assert_eq!(res, str("hel")), + ) + .await + .unwrap(); + + test( + r#" + let str = "hello" + <: str.slice(3, -1) + "#, + |res| assert_eq!(res, str("")), + ) + .await + .unwrap(); + + test( + r#" + let str = "hello" + <: str.slice(-1, -3) + "#, + |res| assert_eq!(res, str("")), + ) + .await + .unwrap(); + + test( + r#" + let str = "hello" + <: str.slice(-3, -1) + "#, + |res| assert_eq!(res, str("")), + ) + .await + .unwrap(); + + test( + r#" + let str = "hello" + <: str.slice(11, 13) + "#, + |res| assert_eq!(res, str("")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn codepoint_at() { + test( + r#" + let str = "𩸽" + <: str.codepoint_at(0) + "#, + |res| assert_eq!(res, num(171581)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_arr() { + test( + r#" + let str = "𩸽👉🏿👨‍👦" + <: str.to_arr() + "#, + |res| assert_eq!(res, arr([str("𩸽"), str("👉🏿"), str("👨‍👦")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_unicode_arr() { + test( + r#" + let str = "𩸽👉🏿👨‍👦" + <: str.to_unicode_arr() + "#, + |res| { + assert_eq!( + res, + arr([ + str("𩸽"), + str("👉"), + str("\u{1F3FF}"), + str("👨"), + str("\u{200d}"), + str("👦") + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_unicode_codepoint_arr() { + test( + r#" + let str = "𩸽👉🏿👨‍👦" + <: str.to_unicode_codepoint_arr() + "#, + |res| { + assert_eq!( + res, + arr([ + num(171581), + num(128073), + num(127999), + num(128104), + num(8205), + num(128102) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_char_arr() { + test( + r#" + let str = "abc𩸽👉🏿👨‍👦def" + <: str.to_char_arr() + "#, + |res| { + assert_eq!( + res, + arr([ + 97, 98, 99, 55399, 56893, 55357, 56393, 55356, 57343, 55357, 56424, + 8205, 55357, 56422, 100, 101, 102 + ] + .into_iter() + .map(|u| str(String::from_utf16_lossy(&[u]))) + .collect::>()) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_charcode_arr() { + test( + r#" + let str = "abc𩸽👉🏿👨‍👦def" + <: str.to_charcode_arr() + "#, + |res| { + assert_eq!( + res, + arr([ + num(97), + num(98), + num(99), + num(55399), + num(56893), + num(55357), + num(56393), + num(55356), + num(57343), + num(55357), + num(56424), + num(8205), + num(55357), + num(56422), + num(100), + num(101), + num(102), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_utf8_byte_arr() { + test( + r#" + let str = "abc𩸽👉🏿👨‍👦def" + <: str.to_utf8_byte_arr() + "#, + |res| { + assert_eq!( + res, + arr([ + num(97), + num(98), + num(99), + num(240), + num(169), + num(184), + num(189), + num(240), + num(159), + num(145), + num(137), + num(240), + num(159), + num(143), + num(191), + num(240), + num(159), + num(145), + num(168), + num(226), + num(128), + num(141), + num(240), + num(159), + num(145), + num(166), + num(100), + num(101), + num(102), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn starts_with_no_index() { + test( + r#" + let str = "hello" + let empty = "" + <: [ + str.starts_with(""), str.starts_with("hello"), + str.starts_with("he"), str.starts_with("ell"), + empty.starts_with(""), empty.starts_with("he"), + ] + "#, + |res| { + assert_eq!( + res, + arr([ + bool(true), + bool(true), + bool(true), + bool(false), + bool(true), + bool(false), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn starts_with_with_index() { + test( + r#" + let str = "hello" + let empty = "" + <: [ + str.starts_with("", 4), str.starts_with("he", 0), + str.starts_with("ll", 2), str.starts_with("lo", 3), + str.starts_with("lo", -2), str.starts_with("hel", -5), + str.starts_with("he", 2), str.starts_with("loa", 3), + str.starts_with("lo", -6), str.starts_with("", -7), + str.starts_with("lo", 6), str.starts_with("", 7), + empty.starts_with("", 2), empty.starts_with("ll", 2), + ] + "#, + |res| { + assert_eq!( + res, + arr([ + bool(true), + bool(true), + bool(true), + bool(true), + bool(true), + bool(true), + bool(false), + bool(false), + bool(false), + bool(true), + bool(false), + bool(true), + bool(true), + bool(false), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn ends_with_no_index() { + test( + r#" + let str = "hello" + let empty = "" + <: [ + str.ends_with(""), str.ends_with("hello"), + str.ends_with("lo"), str.ends_with("ell"), + empty.ends_with(""), empty.ends_with("he"), + ] + "#, + |res| { + assert_eq!( + res, + arr([ + bool(true), + bool(true), + bool(true), + bool(false), + bool(true), + bool(false), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn ends_with_with_index() { + test( + r#" + let str = "hello" + let empty = "" + <: [ + // str.ends_with("", 3), str.ends_with("lo", 5), + // str.ends_with("ll", 4), str.ends_with("he", 2), + // str.ends_with("ll", -1), str.ends_with("he", -3), + str.ends_with("he", 5), str.ends_with("lo", 3), + str.ends_with("lo", -6), str.ends_with("", -7), + str.ends_with("lo", 6), str.ends_with("", 7), + empty.ends_with("", 2), empty.ends_with("ll", 2), + ] + "#, + |res| { + assert_eq!( + res, + arr([ + // bool(true), + // bool(true), + // bool(true), + // bool(true), + // bool(true), + // bool(true), + bool(false), + bool(false), + bool(false), + bool(true), + bool(false), + bool(true), + bool(true), + bool(false), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn pad_start() { + test( + r#" + let str = "abc" + <: [ + str.pad_start(0), str.pad_start(1), str.pad_start(2), + str.pad_start(3), str.pad_start(4), str.pad_start(5), + str.pad_start(0, "0"), str.pad_start(1, "0"), str.pad_start(2, "0"), + str.pad_start(3, "0"), str.pad_start(4, "0"), str.pad_start(5, "0"), + str.pad_start(0, "01"), str.pad_start(1, "01"), str.pad_start(2, "01"), + str.pad_start(3, "01"), str.pad_start(4, "01"), str.pad_start(5, "01"), + ] + "#, + |res| { + assert_eq!( + res, + arr([ + str("abc"), + str("abc"), + str("abc"), + str("abc"), + str(" abc"), + str(" abc"), + str("abc"), + str("abc"), + str("abc"), + str("abc"), + str("0abc"), + str("00abc"), + str("abc"), + str("abc"), + str("abc"), + str("abc"), + str("0abc"), + str("01abc"), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn pad_end() { + test( + r#" + let str = "abc" + <: [ + str.pad_end(0), str.pad_end(1), str.pad_end(2), + str.pad_end(3), str.pad_end(4), str.pad_end(5), + str.pad_end(0, "0"), str.pad_end(1, "0"), str.pad_end(2, "0"), + str.pad_end(3, "0"), str.pad_end(4, "0"), str.pad_end(5, "0"), + str.pad_end(0, "01"), str.pad_end(1, "01"), str.pad_end(2, "01"), + str.pad_end(3, "01"), str.pad_end(4, "01"), str.pad_end(5, "01"), + ] + "#, + |res| { + assert_eq!( + res, + arr([ + str("abc"), + str("abc"), + str("abc"), + str("abc"), + str("abc "), + str("abc "), + str("abc"), + str("abc"), + str("abc"), + str("abc"), + str("abc0"), + str("abc00"), + str("abc"), + str("abc"), + str("abc"), + str("abc"), + str("abc0"), + str("abc01"), + ]) + ) + }, + ) + .await + .unwrap(); + } + } + + mod arr { + use super::*; + + #[tokio::test] + async fn len() { + test( + r#" + let arr = [1, 2, 3] + <: arr.len + "#, + |res| assert_eq!(res, num(3)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn push() { + test( + r#" + let arr = [1, 2, 3] + arr.push(4) + <: arr + "#, + |res| assert_eq!(res, arr([num(1), num(2), num(3), num(4)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn unshift() { + test( + r#" + let arr = [1, 2, 3] + arr.unshift(4) + <: arr + "#, + |res| assert_eq!(res, arr([num(4), num(1), num(2), num(3)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn pop() { + test( + r#" + let arr = [1, 2, 3] + let popped = arr.pop() + <: [popped, arr] + "#, + |res| assert_eq!(res, arr([num(3), arr([num(1), num(2)])])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn shift() { + test( + r#" + let arr = [1, 2, 3] + let shifted = arr.shift() + <: [shifted, arr] + "#, + |res| assert_eq!(res, arr([num(1), arr([num(2), num(3)])])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn concat() { + test( + r#" + let arr = [1, 2, 3] + let concated = arr.concat([4, 5]) + <: [concated, arr] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([num(1), num(2), num(3), num(4), num(5)]), + arr([num(1), num(2), num(3)]) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn slice() { + test( + r#" + let arr = ["ant", "bison", "camel", "duck", "elephant"] + let sliced = arr.slice(2, 4) + <: [sliced, arr] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([str("camel"), str("duck")]), + arr([ + str("ant"), + str("bison"), + str("camel"), + str("duck"), + str("elephant") + ]) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn slice_out_of_range() { + test( + r#" + let arr = ["ant", "bison", "camel", "duck", "elephant"] + <: arr.slice(4, 2) + "#, + |res| assert_eq!(res, arr([])), + ) + .await + .unwrap(); + + test( + r#" + let arr = ["ant", "bison", "camel", "duck", "elephant"] + <: arr.slice(-2, 4) + "#, + |res| assert_eq!(res, arr([str("duck")])), + ) + .await + .unwrap(); + + test( + r#" + let arr = ["ant", "bison", "camel", "duck", "elephant"] + <: arr.slice(4, -2) + "#, + |res| assert_eq!(res, arr([])), + ) + .await + .unwrap(); + + test( + r#" + let arr = ["ant", "bison", "camel", "duck", "elephant"] + <: arr.slice(-2, -4) + "#, + |res| assert_eq!(res, arr([])), + ) + .await + .unwrap(); + + test( + r#" + let arr = ["ant", "bison", "camel", "duck", "elephant"] + <: arr.slice(-4, -2) + "#, + |res| assert_eq!(res, arr([str("bison"), str("camel")])), + ) + .await + .unwrap(); + + test( + r#" + let arr = ["ant", "bison", "camel", "duck", "elephant"] + <: arr.slice(12, 14) + "#, + |res| assert_eq!(res, arr([])), + ) + .await + .unwrap(); + + test( + r#" + let arr = ["ant", "bison", "camel", "duck", "elephant"] + <: arr.slice(-14, -12) + "#, + |res| assert_eq!(res, arr([])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn join() { + test( + r#" + let arr = ["a", "b", "c"] + <: arr.join("-") + "#, + |res| assert_eq!(res, str("a-b-c")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn map() { + test( + r#" + let arr = [1, 2, 3] + <: arr.map(@(item) { item * 2 }) + "#, + |res| assert_eq!(res, arr([num(2), num(4), num(6)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn map_with_index() { + test( + r#" + let arr = [1, 2, 3] + <: arr.map(@(item, index) { item * index }) + "#, + |res| assert_eq!(res, arr([num(0), num(2), num(6)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn filter() { + test( + r#" + let arr = [1, 2, 3] + <: arr.filter(@(item) { item != 2 }) + "#, + |res| assert_eq!(res, arr([num(1), num(3)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn filter_with_index() { + test( + r#" + let arr = [1, 2, 3, 4] + <: arr.filter(@(item, index) { item != 2 && index != 3 }) + "#, + |res| assert_eq!(res, arr([num(1), num(3)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn reduce() { + test( + r#" + let arr = [1, 2, 3, 4] + <: arr.reduce(@(accumulator, currentValue) { (accumulator + currentValue) }) + "#, + |res| assert_eq!(res, num(10)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn reduce_with_index() { + test( + r#" + let arr = [1, 2, 3, 4] + <: arr.reduce(@(accumulator, currentValue, index) { (accumulator + (currentValue * index)) } 0) + "#, + |res| assert_eq!(res, num(20)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn reduce_of_empty_array_without_initial_value() { + let err = test( + r#" + let arr = [1, 2, 3, 4] + <: [].reduce(@(){}) + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!( + err, + AiScriptError::Runtime(AiScriptRuntimeError::Runtime(message)) + if &message == "Reduce of empty array without initial value" + )); + } + + #[tokio::test] + async fn find() { + test( + r#" + let arr = ["abc", "def", "ghi"] + <: arr.find(@(item) { item.incl("e") }) + "#, + |res| assert_eq!(res, str("def")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn find_with_index() { + test( + r#" + let arr = ["abc1", "def1", "ghi1", "abc2", "def2", "ghi2"] + <: arr.find(@(item, index) { item.incl("e") && index > 1 }) + "#, + |res| assert_eq!(res, str("def2")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn incl() { + test( + r#" + let arr = ["abc", "def", "ghi"] + <: [arr.incl("def"), arr.incl("jkl")] + "#, + |res| assert_eq!(res, arr([bool(true), bool(false)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn index_of() { + test( + r#" + let arr = [0,1,2,3,4,0,1,2,3,4] + <: [ + arr.index_of(3) == 3, + arr.index_of(5) == -1, + arr.index_of(3, 3) == 3, + arr.index_of(3, 4) == 8, + arr.index_of(3, -1) == -1, + arr.index_of(3, -2) == 8, + arr.index_of(3, -7) == 3, + arr.index_of(3, 10) == -1, + ].map(@(v){if (v) '1' else '0'}).join() + "#, + |res| assert_eq!(res, str("11111111")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn reverse() { + test( + r#" + let arr = [1, 2, 3] + arr.reverse() + <: arr + "#, + |res| assert_eq!(res, arr([num(3), num(2), num(1)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn copy() { + test( + r#" + let arr = [1, 2, 3] + let copied = arr.copy() + copied.reverse() + <: [copied, arr] + "#, + |res| { + assert_eq!( + res, + arr([arr([num(3), num(2), num(1)]), arr([num(1), num(2), num(3)])]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn sort_num_array() { + test( + r#" + var arr = [2, 10, 3] + let comp = @(a, b) { a - b } + arr.sort(comp) + <: arr + "#, + |res| assert_eq!(res, arr([num(2), num(3), num(10)])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn sort_string_array_with_str_lt() { + test( + r#" + var arr = ["hoge", "huga", "piyo", "hoge"] + arr.sort(Str:lt) + <: arr + "#, + |res| { + assert_eq!( + res, + arr([str("hoge"), str("hoge"), str("huga"), str("piyo")]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn sort_string_array_with_str_gt() { + test( + r#" + var arr = ["hoge", "huga", "piyo", "hoge"] + arr.sort(Str:gt) + <: arr + "#, + |res| { + assert_eq!( + res, + arr([str("piyo"), str("huga"), str("hoge"), str("hoge")]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn sort_object_array() { + test( + r#" + var arr = [{x: 2}, {x: 10}, {x: 3}] + let comp = @(a, b) { a.x - b.x } + + arr.sort(comp) + <: arr + "#, + |res| { + assert_eq!( + res, + arr([ + obj([("x", num(2))]), + obj([("x", num(3))]), + obj([("x", num(10))]) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn fill() { + test( + r#" + var arr1 = [0, 1, 2] + let arr2 = arr1.fill(3) + let arr3 = [0, 1, 2].fill(3, 1) + let arr4 = [0, 1, 2].fill(3, 1, 2) + let arr5 = [0, 1, 2].fill(3, -2, -1) + <: [arr1, arr2, arr3, arr4, arr5] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([num(3), num(3), num(3)]), //target changed + arr([num(3), num(3), num(3)]), + arr([num(0), num(3), num(3)]), + arr([num(0), num(3), num(2)]), + arr([num(0), num(3), num(2)]), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn repeat() { + test( + r#" + var arr1 = [0, 1, 2] + let arr2 = arr1.repeat(3) + let arr3 = arr1.repeat(0) + <: [arr1, arr2, arr3] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([num(0), num(1), num(2)]), // target not changed + arr([ + num(0), + num(1), + num(2), + num(0), + num(1), + num(2), + num(0), + num(1), + num(2), + ]), + arr([]), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn splice_full() { + test( + r#" + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(1, 2, [10]) + <: [arr1, arr2] + "#, + |res| { + assert_eq!( + res, + arr([arr([num(0), num(10), num(3)]), arr([num(1), num(2)]),]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn splice_negative_index() { + test( + r#" + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(-1, 0, [10, 20]) + <: [arr1, arr2] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([num(0), num(1), num(2), num(10), num(20), num(3)]), + arr([]), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn splice_larger_index() { + test( + r#" + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(4, 100, [10, 20]) + <: [arr1, arr2] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([num(0), num(1), num(2), num(3), num(10), num(20)]), + arr([]), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn splice_single_argument() { + test( + r#" + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(1) + <: [arr1, arr2] + "#, + |res| assert_eq!(res, arr([arr([num(0)]), arr([num(1), num(2), num(3)]),])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn flat() { + test( + r#" + var arr1 = [0, [1], [2, 3], [4, [5, 6]]] + let arr2 = arr1.flat() + let arr3 = arr1.flat(2) + <: [arr1, arr2, arr3] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([ + num(0), + arr([num(1)]), + arr([num(2), num(3)]), + arr([num(4), arr([num(5), num(6)])]) + ]), // target not changed + arr([ + num(0), + num(1), + num(2), + num(3), + num(4), + arr([num(5), num(6)]), + ]), + arr([num(0), num(1), num(2), num(3), num(4), num(5), num(6),]), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn flat_map() { + test( + r#" + let arr1 = [0, 1, 2] + let arr2 = ["a", "b"] + let arr3 = arr1.flat_map(@(x){ arr2.map(@(y){ [x, y] }) }) + <: [arr1, arr3] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([num(0), num(1), num(2)]), // target not changed + arr([ + arr([num(0), str("a")]), + arr([num(0), str("b")]), + arr([num(1), str("a")]), + arr([num(1), str("b")]), + arr([num(2), str("a")]), + arr([num(2), str("b")]), + ]), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn every() { + test( + r#" + let arr1 = [0, 1, 2, 3] + let res1 = arr1.every(@(v,i){v==0 || i > 0}) + let res2 = arr1.every(@(v,i){v==0 && i > 0}) + let res3 = [].every(@(v,i){false}) + <: [arr1, res1, res2, res3] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([num(0), num(1), num(2), num(3)]), // target not changed + bool(true), + bool(false), + bool(true), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn some() { + test( + r#" + let arr1 = [0, 1, 2, 3] + let res1 = arr1.some(@(v,i){v%2==0 && i <= 2}) + let res2 = arr1.some(@(v,i){v%2==0 && i > 2}) + <: [arr1, res1, res2] + "#, + |res| { + assert_eq!( + res, + arr([ + arr([num(0), num(1), num(2), num(3)]), // target not changed + bool(true), + bool(false), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn insert() { + test( + r#" + let arr1 = [0, 1, 2] + let res = [] + res.push(arr1.insert(3, 10)) // [0, 1, 2, 10] + res.push(arr1.insert(2, 20)) // [0, 1, 20, 2, 10] + res.push(arr1.insert(0, 30)) // [30, 0, 1, 20, 2, 10] + res.push(arr1.insert(-1, 40)) // [30, 0, 1, 20, 2, 40, 10] + res.push(arr1.insert(-4, 50)) // [30, 0, 1, 50, 20, 2, 40, 10] + res.push(arr1.insert(100, 60)) // [30, 0, 1, 50, 20, 2, 40, 10, 60] + res.push(arr1) + <: res + "#, + |res| { + assert_eq!( + res, + arr([ + null(), + null(), + null(), + null(), + null(), + null(), + arr([ + num(30), + num(0), + num(1), + num(50), + num(20), + num(2), + num(40), + num(10), + num(60) + ]) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn remove() { + test( + r#" + let arr1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + let res = [] + res.push(arr1.remove(9)) // 9 [0, 1, 2, 3, 4, 5, 6, 7, 8] + res.push(arr1.remove(3)) // 3 [0, 1, 2, 4, 5, 6, 7, 8] + res.push(arr1.remove(0)) // 0 [1, 2, 4, 5, 6, 7, 8] + res.push(arr1.remove(-1)) // 8 [1, 2, 4, 5, 6, 7] + res.push(arr1.remove(-5)) // 2 [1, 4, 5, 6, 7] + res.push(arr1.remove(100)) // null [1, 4, 5, 6, 7] + res.push(arr1) + <: res + "#, + |res| { + assert_eq!( + res, + arr([ + num(9), + num(3), + num(0), + num(8), + num(2), + null(), + arr([num(1), num(4), num(5), num(6), num(7)]) + ]) + ) + }, + ) + .await + .unwrap(); + } + } +} + +mod std { + use super::*; + + mod core { + use super::*; + + #[tokio::test] + async fn range() { + test("<: Core:range(1, 10)", |res| { + assert_eq!( + res, + arr([ + num(1), + num(2), + num(3), + num(4), + num(5), + num(6), + num(7), + num(8), + num(9), + num(10) + ]) + ) + }) + .await + .unwrap(); + + test("<: Core:range(1, 1)", |res| assert_eq!(res, arr([num(1),]))) + .await + .unwrap(); + + test("<: Core:range(9, 7)", |res| { + assert_eq!(res, arr([num(9), num(8), num(7),])) + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_str() { + test(r#"<: Core:to_str("abc")"#, |res| { + assert_eq!(res, str("abc")) + }) + .await + .unwrap(); + + test(r#"<: Core:to_str(123)"#, |res| assert_eq!(res, str("123"))) + .await + .unwrap(); + + test(r#"<: Core:to_str(true)"#, |res| { + assert_eq!(res, str("true")) + }) + .await + .unwrap(); + + test(r#"<: Core:to_str(false)"#, |res| { + assert_eq!(res, str("false")) + }) + .await + .unwrap(); + + test(r#"<: Core:to_str(null)"#, |res| { + assert_eq!(res, str("null")) + }) + .await + .unwrap(); + + test(r#"<: Core:to_str({ a: "abc", b: 1234 })"#, |res| { + assert_eq!(res, str(r#"{ a: "abc", b: 1234 }"#)) + }) + .await + .unwrap(); + + test(r#"<: Core:to_str([ true, 123, null ])"#, |res| { + assert_eq!(res, str("[ true, 123, null ]")) + }) + .await + .unwrap(); + + test(r#"<: Core:to_str(@( a, b, c ) {})"#, |res| { + assert_eq!(res, str("@( a, b, c ) { ... }")) + }) + .await + .unwrap(); + + test( + r#" + let arr = [] + arr.push(arr) + <: Core:to_str(arr) + "#, + |res| assert_eq!(res, str("[ ... ]")), + ) + .await + .unwrap(); + + test( + r#" + let arr = [] + arr.push({ value: arr }) + <: Core:to_str(arr) + "#, + |res| assert_eq!(res, str("[ { value: ... } ]")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn abort() { + let err = test(r#"Core:abort("hoge")"#, |_| {}).await.unwrap_err(); + assert!(matches!( + err, + AiScriptError::Runtime(AiScriptRuntimeError::User(message)) + if message == "hoge" + )); + } + } + + mod arr { + use super::*; + + #[tokio::test] + async fn create() { + test("<: Arr:create(0)", |res| assert_eq!(res, arr([]))) + .await + .unwrap(); + + test("<: Arr:create(3)", |res| { + assert_eq!(res, arr([null(), null(), null()])) + }) + .await + .unwrap(); + + test("<: Arr:create(3, 1)", |res| { + assert_eq!(res, arr([num(1), num(1), num(1)])) + }) + .await + .unwrap(); + } + } + + mod math { + use super::*; + + #[tokio::test] + async fn trig() { + test("<: Math:sin(Math:PI / 2)", |res| assert_eq!(res, num(1))) + .await + .unwrap(); + + test("<: Math:sin(0 - (Math:PI / 2))", |res| { + assert_eq!(res, num(-1)) + }) + .await + .unwrap(); + + test("<: Math:sin(Math:PI / 4) * Math:cos(Math:PI / 4)", |res| { + assert!((f64::try_from(res.value).unwrap() - 0.5).abs() <= f64::EPSILON) + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn abs() { + test("<: Math:abs(1 - 6)", |res| assert_eq!(res, num(5))) + .await + .unwrap(); + } + + #[tokio::test] + async fn pow_and_sqrt() { + test("<: Math:sqrt(3^2 + 4^2)", |res| assert_eq!(res, num(5))) + .await + .unwrap(); + } + + #[tokio::test] + async fn round() { + test("<: Math:round(3.14)", |res| assert_eq!(res, num(3))) + .await + .unwrap(); + + test("<: Math:round(-1.414213)", |res| assert_eq!(res, num(-1))) + .await + .unwrap(); + } + + #[tokio::test] + async fn ceil() { + test("<: Math:ceil(2.71828)", |res| assert_eq!(res, num(3))) + .await + .unwrap(); + + test("<: Math:ceil(0 - Math:PI)", |res| assert_eq!(res, num(-3))) + .await + .unwrap(); + + test("<: Math:ceil(1 / Math:Infinity)", |res| { + assert_eq!(res, num(0)) + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn floor() { + test("<: Math:floor(23.14069)", |res| assert_eq!(res, num(23))) + .await + .unwrap(); + + test("<: Math:floor(Math:Infinity / 0)", |res| { + assert_eq!(res, num(f64::INFINITY)) + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn min() { + test("<: Math:min(2, 3)", |res| assert_eq!(res, num(2))) + .await + .unwrap(); + } + + #[tokio::test] + async fn max() { + test("<: Math:max(-2, -3)", |res| assert_eq!(res, num(-2))) + .await + .unwrap(); + } + + #[tokio::test] + async fn rnd_with_arg() { + test("<: Math:rnd(1, 1.5)", |res| assert_eq!(res, num(1))) + .await + .unwrap(); + } + + #[tokio::test] + async fn gen_rng() { + // 2つのシード値から1~maxの乱数をn回生成して一致率を見る + test( + r#" + @test(seed1, seed2) { + let n = 100 + let max = 100000 + let threshold = 0.05 + let random1 = Math:gen_rng(seed1) + let random2 = Math:gen_rng(seed2) + var same = 0 + for n { + if random1(1, max) == random2(1, max) { + same += 1 + } + } + let rate = same / n + if seed1 == seed2 { rate == 1 } + else { rate < threshold } + } + let seed1 = `{Util:uuid()}` + let seed2 = `{Date:year()}` + <: [ + test(seed1, seed1) + test(seed1, seed2) + ] + "#, + |res| assert_eq!(res, arr([bool(true), bool(true)])), + ) + .await + .unwrap(); + } + } + + mod obj { + use super::*; + + #[tokio::test] + async fn keys() { + test( + r#" + let o = { a: 1; b: 2; c: 3; } + + <: Obj:keys(o) + "#, + |res| assert_eq!(res, arr([str("a"), str("b"), str("c")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn vals() { + test( + r#" + let o = { _nul: null; _num: 24; _str: 'hoge'; _arr: []; _obj: {}; } + + <: Obj:vals(o) + "#, + |res| { + assert_eq!( + res, + arr([ + null(), + num(24), + str("hoge"), + arr([]), + obj([] as [(String, Value); 0]) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn kvs() { + test( + r#" + let o = { a: 1; b: 2; c: 3; } + + <: Obj:kvs(o) + "#, + |res| { + assert_eq!( + res, + arr([ + arr([str("a"), num(1)]), + arr([str("b"), num(2)]), + arr([str("c"), num(3)]) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn merge() { + test( + r#" + let o1 = { a: 1; b: 2; } + let o2 = { b: 3; c: 4; } + + <: Obj:merge(o1, o2) + "#, + |res| assert_eq!(res, obj([("a", num(1)), ("b", num(3)), ("c", num(4)),])), + ) + .await + .unwrap(); + } + } + + mod str { + use super::*; + + #[tokio::test] + async fn lf() { + test( + r#" + <: Str:lf + "#, + |res| assert_eq!(res, str("\n")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn from_codepoint() { + test( + r#" + <: Str:from_codepoint(65) + "#, + |res| assert_eq!(res, str("A")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn from_unicode_codepoints() { + test( + r#" + <: Str:from_unicode_codepoints([171581, 128073, 127999, 128104, 8205, 128102]) + "#, + |res| assert_eq!(res, str("𩸽👉🏿👨‍👦")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn from_utf8_bytes() { + test( + r#" + <: Str:from_utf8_bytes([240, 169, 184, 189, 240, 159, 145, 137, 240, 159, 143, 191, 240, 159, 145, 168, 226, 128, 141, 240, 159, 145, 166]) + "#, + |res| assert_eq!(res, str("𩸽👉🏿👨‍👦")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn charcode_at() { + test( + r#" + <: "aiscript".split().map(@(x, _) { x.charcode_at(0) }) + "#, + |res| { + assert_eq!( + res, + arr([ + num(97), + num(105), + num(115), + num(99), + num(114), + num(105), + num(112), + num(116), + ]) + ) + }, + ) + .await + .unwrap(); + } + } + + mod uri { + use super::*; + + #[tokio::test] + async fn encode_full() { + test( + r#" + <: Uri:encode_full("https://example.com/?q=あいちゃん") + "#, + |res| { + assert_eq!( + res, + str("https://example.com/?q=%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn encode_component() { + test( + r#" + <: Uri:encode_component("https://example.com/?q=あいちゃん") + "#, + |res| assert_eq!(res, str("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn decode_full() { + test( + r#" + <: Uri:decode_full("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") + "#, + |res| assert_eq!(res, str("https%3A%2F%2Fexample.com%2F%3Fq%3Dあいちゃん")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn decode_component() { + test( + r#" + <: Uri:decode_component("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") + "#, + |res| assert_eq!(res, str("https://example.com/?q=あいちゃん")), + ) + .await + .unwrap(); + } + } + + mod error { + use super::*; + + #[tokio::test] + async fn create() { + test( + r#" + <: Error:create('ai', {chan: 'kawaii'}) + "#, + |res| assert_eq!(res, error("ai", Some(obj([("chan", str("kawaii"))])))), + ) + .await + .unwrap(); + } + } + + mod json { + use super::*; + + #[tokio::test] + async fn stringify_fn() { + test( + r#" + <: Json:stringify(@(){}) + "#, + |res| assert_eq!(res, str(r#""""#)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn parsable() { + test( + r#" + <: [ + Json:parsable('null') + Json:stringify(Json:parse('null')) + ] + "#, + |res| assert_eq!(res, arr([bool(true), str("null")])), + ) + .await + .unwrap(); + + test( + r#" + <: [ + Json:parsable('"hoge"') + Json:stringify(Json:parse('"hoge"')) + ] + "#, + |res| assert_eq!(res, arr([bool(true), str(r#""hoge""#)])), + ) + .await + .unwrap(); + + test( + r#" + <: [ + Json:parsable('[]') + Json:stringify(Json:parse('[]')) + ] + "#, + |res| assert_eq!(res, arr([bool(true), str("[]")])), + ) + .await + .unwrap(); + + test( + r#" + <: [ + Json:parsable('{}') + Json:stringify(Json:parse('{}')) + ] + "#, + |res| assert_eq!(res, arr([bool(true), str("{}")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn unparsable() { + test( + r#" + <: [ + Json:parsable('') + Json:stringify(Json:parse('')) + ] + "#, + |res| assert_eq!(res, arr([bool(false), error("not_json", None)])), + ) + .await + .unwrap(); + + test( + r#" + <: [ + Json:parsable('hoge') + Json:stringify(Json:parse('hoge')) + ] + "#, + |res| assert_eq!(res, arr([bool(false), error("not_json", None)])), + ) + .await + .unwrap(); + + test( + r#" + <: [ + Json:parsable('[') + Json:stringify(Json:parse('[')) + ] + "#, + |res| assert_eq!(res, arr([bool(false), error("not_json", None)])), + ) + .await + .unwrap(); + } + } + + mod date { + use chrono::{Datelike, Local, NaiveDate, TimeZone, Timelike}; + + use super::*; + + #[tokio::test] + async fn year() { + let example_time = NaiveDate::from_ymd_opt(2024, 1, 2) + .unwrap() + .and_hms_milli_opt(3, 4, 5, 6) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .timestamp_millis(); + test( + &format!( + " + <: [Date:year(0), Date:year({example_time})] + " + ), + |res| { + assert_eq!( + res, + arr([ + num(Local.timestamp_millis_opt(0).unwrap().year()), + num(2024) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn month() { + let example_time = NaiveDate::from_ymd_opt(2024, 1, 2) + .unwrap() + .and_hms_milli_opt(3, 4, 5, 6) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .timestamp_millis(); + test( + &format!( + " + <: [Date:month(0), Date:month({example_time})] + " + ), + |res| { + assert_eq!( + res, + arr([num(Local.timestamp_millis_opt(0).unwrap().month()), num(1)]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn day() { + let example_time = NaiveDate::from_ymd_opt(2024, 1, 2) + .unwrap() + .and_hms_milli_opt(3, 4, 5, 6) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .timestamp_millis(); + test( + &format!( + " + <: [Date:day(0), Date:day({example_time})] + " + ), + |res| { + assert_eq!( + res, + arr([num(Local.timestamp_millis_opt(0).unwrap().day()), num(2)]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn hour() { + let example_time = NaiveDate::from_ymd_opt(2024, 1, 2) + .unwrap() + .and_hms_milli_opt(3, 4, 5, 6) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .timestamp_millis(); + test( + &format!( + " + <: [Date:hour(0), Date:hour({example_time})] + " + ), + |res| { + assert_eq!( + res, + arr([num(Local.timestamp_millis_opt(0).unwrap().hour()), num(3)]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn minute() { + let example_time = NaiveDate::from_ymd_opt(2024, 1, 2) + .unwrap() + .and_hms_milli_opt(3, 4, 5, 6) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .timestamp_millis(); + test( + &format!( + " + <: [Date:minute(0), Date:minute({example_time})] + " + ), + |res| { + assert_eq!( + res, + arr([num(Local.timestamp_millis_opt(0).unwrap().minute()), num(4)]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn second() { + let example_time = NaiveDate::from_ymd_opt(2024, 1, 2) + .unwrap() + .and_hms_milli_opt(3, 4, 5, 6) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .timestamp_millis(); + test( + &format!( + " + <: [Date:second(0), Date:second({example_time})] + " + ), + |res| { + assert_eq!( + res, + arr([num(Local.timestamp_millis_opt(0).unwrap().second()), num(5)]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn millisecond() { + let example_time = NaiveDate::from_ymd_opt(2024, 1, 2) + .unwrap() + .and_hms_milli_opt(3, 4, 5, 6) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .timestamp_millis(); + test( + &format!( + " + <: [Date:millisecond(0), Date:millisecond({example_time})] + " + ), + |res| { + assert_eq!( + res, + arr([ + num( + (Local.timestamp_millis_opt(0).unwrap().timestamp_millis() % 1000) + as f64 + ), + num(6) + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_iso_str() { + test( + r#" + let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") + let s1 = Date:to_iso_str(d1) + let d2 = Date:parse(s1) + <: [d1, d2, s1] + "#, + |res| { + let res = >::try_from(res).unwrap(); + assert_eq!(res[0], res[1]); + let s1 = String::try_from(res[2].clone()).unwrap(); + regex::Regex::new( + r"(?x) + ^[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}T + [0-9]{2,2}:[0-9]{2,2}:[0-9]{2,2}\.[0-9]{3,3} + (Z|[-+][0-9]{2,2}:[0-9]{2,2})$ + ", + ) + .unwrap() + .captures(&s1); + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_iso_str_utc() { + test( + r#" + let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") + let s1 = Date:to_iso_str(d1, 0) + let d2 = Date:parse(s1) + <: [d1, d2, s1] + "#, + |res| { + assert_eq!( + res, + arr([ + num(1712854066021.0), + num(1712854066021.0), + str("2024-04-11T16:47:46.021Z") + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_iso_str_09_00() { + test( + r#" + let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") + let s1 = Date:to_iso_str(d1, 9*60) + let d2 = Date:parse(s1) + <: [d1, d2, s1] + "#, + |res| { + assert_eq!( + res, + arr([ + num(1712854066021.0), + num(1712854066021.0), + str("2024-04-12T01:47:46.021+09:00") + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn to_iso_str_05_18() { + test( + r#" + let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") + let s1 = Date:to_iso_str(d1, -5*60-18) + let d2 = Date:parse(s1) + <: [d1, d2, s1] + "#, + |res| { + assert_eq!( + res, + arr([ + num(1712854066021.0), + num(1712854066021.0), + str("2024-04-11T11:29:46.021-05:18") + ]) + ) + }, + ) + .await + .unwrap(); + } + } +} + +mod unicode { + use super::*; + + #[tokio::test] + async fn len() { + test( + r#" + <: "👍🏽🍆🌮".len + "#, + |res| assert_eq!(res, num(3)), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn pick() { + test( + r#" + <: "👍🏽🍆🌮".pick(1) + "#, + |res| assert_eq!(res, str("🍆")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn slice() { + test( + r#" + <: "Emojis 👍🏽 are 🍆 poison. 🌮s are bad.".slice(7, 14) + "#, + |res| assert_eq!(res, str("👍🏽 are 🍆")), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn split() { + test( + r#" + <: "👍🏽🍆🌮".split() + "#, + |res| assert_eq!(res, arr([str("👍🏽"), str("🍆"), str("🌮")])), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn charcode_at() { + test( + r#" + <: [ + "👍🏽🍆🌮".charcode_at(0), + "👍🏽🍆🌮".charcode_at(1), + "👍🏽🍆🌮".charcode_at(2), + "👍🏽🍆🌮".charcode_at(3), + "👍🏽🍆🌮".charcode_at(4), + "👍🏽🍆🌮".charcode_at(5), + "👍🏽🍆🌮".charcode_at(6), + "👍🏽🍆🌮".charcode_at(7), + ] + "#, + |res| { + assert_eq!( + res, + arr([ + num(55357), + num(56397), + num(55356), + num(57341), + num(55356), + num(57158), + num(55356), + num(57134), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn codepoint_at() { + test( + r#" + <: [ + "👍🏽🍆🌮".codepoint_at(0), + "👍🏽🍆🌮".codepoint_at(1), + "👍🏽🍆🌮".codepoint_at(2), + "👍🏽🍆🌮".codepoint_at(3), + "👍🏽🍆🌮".codepoint_at(4), + "👍🏽🍆🌮".codepoint_at(5), + "👍🏽🍆🌮".codepoint_at(6), + "👍🏽🍆🌮".codepoint_at(7), + ] + "#, + |res| { + assert_eq!( + res, + arr([ + num(128077), + num(56397), + num(127997), + num(57341), + num(127814), + num(57158), + num(127790), + num(57134), + ]) + ) + }, + ) + .await + .unwrap(); + } +} + +mod security { + use super::*; + + #[tokio::test] + async fn cannot_access_js_native_property_via_var() { + let err = test( + r#" + <: constructor + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + + let err = test( + r#" + <: prototype + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + + let err = test( + r#" + <: __proto__ + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + } + + #[tokio::test] + async fn cannot_access_js_native_property_via_object() { + test( + r#" + let obj = {} + + <: obj.constructor + "#, + |res| assert_eq!(res, null()), + ) + .await + .unwrap(); + + test( + r#" + let obj = {} + + <: obj.prototype + "#, + |res| assert_eq!(res, null()), + ) + .await + .unwrap(); + + test( + r#" + let obj = {} + + <: obj.__proto__ + "#, + |res| assert_eq!(res, null()), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn cannot_access_js_native_property_via_primitive_prop() { + let err = test( + r#" + <: "".constructor + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + + let err = test( + r#" + <: "".prototype + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + + let err = test( + r#" + <: "".__proto__ + "#, + |_| {}, + ) + .await + .unwrap_err(); + assert!(matches!(err, AiScriptError::Runtime(_))); + } +} + +mod extra { + use super::*; + + #[tokio::test] + async fn fizz_buzz() { + test( + r#" + let res = [] + for (let i = 1, 15) { + let msg = + if (i % 15 == 0) "FizzBuzz" + elif (i % 3 == 0) "Fizz" + elif (i % 5 == 0) "Buzz" + else i + res.push(msg) + } + <: res + "#, + |res| { + assert_eq!( + res, + arr([ + num(1), + num(2), + str("Fizz"), + num(4), + str("Buzz"), + str("Fizz"), + num(7), + num(8), + str("Fizz"), + str("Buzz"), + num(11), + str("Fizz"), + num(13), + num(14), + str("FizzBuzz"), + ]) + ) + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn ski() { + test( + r#" + let s = @(x) { @(y) { @(z) { + //let f = x(z) f(@(a){ let g = y(z) g(a) }) + let f = x(z) + f(y(z)) + }}} + let k = @(x){ @(y) { x } } + let i = @(x){ x } + + // combine + @c(l) { + // extract + @x(v) { + if (Core:type(v) == "arr") { c(v) } else { v } + } + + // rec + @r(f, n) { + if (n < l.len) { + r(f(x(l[n])), (n + 1)) + } else { f } + } + + r(x(l[0]), 1) + } + + let sksik = [s, [k, [s, i]], k] + c([sksik, "foo", print]) + "#, + |res| assert_eq!(res, str("foo")), + ) + .await + .unwrap(); + } +}