diff --git a/.gitignore b/.gitignore index a221ac10..5b894527 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ .vscode/ +test_codebase/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9ba3de64..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: rust -rust: - - stable -branches: - only: - - master -cache: cargo -before_cache: - - cargo-cache -install: - - rustc -Vv - - cargo -V - - cargo install cargo-cache --no-default-features --features ci-autoclean cargo-cache -script: - - cargo build --verbose --all - - cargo test --verbose --all diff --git a/CONFIGURING.md b/CONFIGURING.md index 34a59dc0..a868fafa 100644 --- a/CONFIGURING.md +++ b/CONFIGURING.md @@ -50,6 +50,7 @@ Raised by DreamChecker: * `control_condition_static` - Raised on a control condition such as `if`/`while` having a static condition such as `1` or `"string"` * `if_condition_determinate` - Raised on if condition being always true or always false * `loop_condition_determinate` - Raised on loop condition such as in `for` being always true or always false +* `improper_index` - Raised on accessing a non list with [] Raised by Lexer: diff --git a/Cargo.lock b/Cargo.lock index 0c935f37..249ccbac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adler32" @@ -14,57 +14,130 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] -name = "ansi_term" -version = "0.12.1" +name = "android-tzdata" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ - "winapi", + "libc", ] [[package]] -name = "atty" -version = "0.2.14" +name = "anstream" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "attribute-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c124f12ade4e670107b132722d0ad1a5c9790bcbc1b265336369ea05626b4498" +dependencies = [ + "attribute-derive-macro", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b217a07446e0fb086f83401a98297e2d81492122f5874db5391bd270a185f88" +dependencies = [ + "collection_literals", + "interpolator", + "proc-macro-error", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.106", ] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" -version = "0.12.3" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bincode" @@ -81,253 +154,280 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + [[package]] name = "block-buffer" -version = "0.7.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "block-padding", - "byte-tools", - "byteorder", "generic-array", ] -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "memchr", -] - [[package]] name = "builtins-proc-macro" version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.7.3" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.0.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] -name = "byteorder" -version = "1.4.3" +name = "bytes" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.0.72" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", + "libc", + "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ - "libc", - "num-integer", + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", - "time", - "winapi", -] - -[[package]] -name = "chrono-tz" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" -dependencies = [ - "parse-zoneinfo", - "phf", - "phf_codegen", + "wasm-bindgen", + "windows-link", ] [[package]] name = "clap" -version = "2.34.0" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim", - "textwrap", - "unicode-width", - "vec_map", + "clap_builder", + "clap_derive", ] +[[package]] +name = "clap_builder" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "collection_literals" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b3f65b8fb8e88ba339f7d23a390fe1b0896217da05e2a66c584c9b29a91df8" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "color_space" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3776b2bcc4e914db501bb9be9572dd706e344b9eb8f882894f3daa651d281381" +checksum = "52fdfaf2bee6357023bf7f95b15a8ef0b82759d2bce705cc45efcae9ae10f0ff" [[package]] -name = "crc32fast" -version = "1.3.0" +name = "colorchoice" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "cfg-if", + "libc", ] [[package]] -name = "crossbeam-channel" -version = "0.5.1" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", - "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.5" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "cfg-if", "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.5" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "cfg-if", - "lazy_static", + "generic-array", + "typenum", ] [[package]] name = "dap-types" version = "0.0.0" dependencies = [ - "ahash", + "foldhash", "serde", "serde_derive", "serde_json", ] [[package]] -name = "deflate" -version = "0.9.1" +name = "derivative" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f95bf05dffba6e6cce8dfbb30def788154949ccd9aed761b472119c21e01c70" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "adler32", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "deunicode" -version = "0.4.3" +name = "derive-where" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] [[package]] name = "digest" -version = "0.8.1" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "generic-array", + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] name = "dm-langserver" -version = "1.5.1" +version = "1.11.0" dependencies = [ - "ahash", "bincode", "chrono", "dap-types", "dreamchecker", "dreammaker", + "foldhash", "git2", - "guard", "interval-tree", "jsonrpc-core", "lazy_static", @@ -337,20 +437,21 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "sha256", + "ureq", "url", ] [[package]] name = "dmdoc" -version = "1.4.1" +version = "1.11.0" dependencies = [ "chrono", "dreammaker", + "foldhash", "git2", + "maud", "pulldown-cmark", - "serde", - "serde_derive", - "tera", "walkdir", ] @@ -358,11 +459,13 @@ dependencies = [ name = "dmm-tools" version = "0.1.0" dependencies = [ - "ahash", "bumpalo", "bytemuck", "dreammaker", + "either", + "foldhash", "gfx_core", + "gif", "indexmap", "inflate", "lodepng", @@ -374,19 +477,18 @@ dependencies = [ [[package]] name = "dmm-tools-cli" -version = "1.3.1" +version = "1.11.0" dependencies = [ - "ahash", "chrono", + "clap", "dmm-tools", "dreammaker", + "foldhash", "git2", "rayon", "serde", "serde_derive", "serde_json", - "structopt", - "structopt-derive", ] [[package]] @@ -395,18 +497,17 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cf9537e2d06891448799b96d5a8c8083e0e90522a7fdabe6ebf4f41d79d651" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "dreamchecker" -version = "1.7.1" +version = "1.11.0" dependencies = [ - "ahash", "chrono", "dreammaker", + "foldhash", "git2", - "guard", "serde_json", ] @@ -414,11 +515,13 @@ dependencies = [ name = "dreammaker" version = "0.1.0" dependencies = [ - "ahash", - "bitflags", + "bitflags 1.3.2", "builtins-proc-macro", "color_space", - "guard", + "derivative", + "foldhash", + "get-size", + "get-size-derive", "indexmap", "interval-tree", "lodepng", @@ -433,150 +536,197 @@ dependencies = [ [[package]] name = "either" -version = "1.6.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "encoding" -version = "0.2.33" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ - "encoding-index-japanese", - "encoding-index-korean", - "encoding-index-simpchinese", - "encoding-index-singlebyte", - "encoding-index-tradchinese", -] - -[[package]] -name = "encoding-index-japanese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-korean" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-simpchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-singlebyte" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-tradchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding_index_tests" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - -[[package]] -name = "fallible_collections" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52db5973b6a19247baf19b30f41c23a1bfffc2e9ce0a5db2f60e3cd5dc8895f7" -dependencies = [ - "hashbrown", + "simd-adler32", ] [[package]] name = "flate2" -version = "1.0.22" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ - "cfg-if", "crc32fast", - "libc", + "libz-rs-sys", "miniz_oxide", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "matches", "percent-encoding", ] [[package]] name = "futures" -version = "0.1.31" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] [[package]] name = "generic-array" -version = "0.12.4" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", + "version_check", +] + +[[package]] +name = "get-size" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b61e2dab7eedce93a83ab3468b919873ff16bac5a3e704011ff836d22b2120" + +[[package]] +name = "get-size-derive" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a1bcfb855c1f340d5913ab542e36f25a1c56f57de79022928297632435dec2" +dependencies = [ + "attribute-derive", + "quote", + "syn 2.0.106", ] [[package]] name = "getopts" -version = "0.2.21" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", ] [[package]] @@ -585,123 +735,190 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75fbddaef2e12b4995900539d7209d947b988a3d87ee8737484d049b526e5441" dependencies = [ - "bitflags", + "bitflags 1.3.2", "draw_state", "log", ] [[package]] -name = "git2" -version = "0.13.25" +name = "gif" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" dependencies = [ - "bitflags", + "color_quant", + "weezl", +] + +[[package]] +name = "git2" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +dependencies = [ + "bitflags 2.9.3", "libc", "libgit2-sys", "log", "url", ] -[[package]] -name = "globset" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" -dependencies = [ - "aho-corasick", - "bstr", - "fnv", - "log", - "regex", -] - -[[package]] -name = "globwalk" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" -dependencies = [ - "bitflags", - "ignore", - "walkdir", -] - -[[package]] -name = "guard" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff893cc51ea04f8a3b73fbaf4376c06ebc5a0ccbe86d460896f805d9417c93ea" - [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "heck" -version = "0.3.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ - "unicode-segmentation", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", ] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "libc", + "cc", ] [[package]] -name = "humansize" -version = "1.1.1" +name = "icu_collections" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] [[package]] name = "idna" -version = "0.2.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "ignore" -version = "0.4.18" +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ - "crossbeam-utils", - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "1.7.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ - "autocfg", + "equivalent", "hashbrown", ] @@ -714,6 +931,12 @@ dependencies = [ "adler32", ] +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + [[package]] name = "interval-tree" version = "0.8.0" @@ -722,27 +945,46 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.1" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.3", "libc", ] [[package]] -name = "jsonrpc-core" -version = "14.2.0" +name = "js-sys" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0747307121ffb9703afd93afbd0fb4f854c38fb873f2c8b90e0e902f27c7b62" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" dependencies = [ "futures", + "futures-executor", + "futures-util", "log", "serde", "serde_derive", @@ -751,21 +993,21 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.112" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libgit2-sys" -version = "0.12.26+1.3.0" +version = "0.18.2+1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" +checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" dependencies = [ "cc", "libc", @@ -774,10 +1016,19 @@ dependencies = [ ] [[package]] -name = "libz-sys" -version = "1.1.3" +name = "libz-rs-sys" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", "libc", @@ -786,12 +1037,18 @@ dependencies = [ ] [[package]] -name = "lodepng" -version = "3.4.7" +name = "litemap" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24844d5c0b922ddd52fb5bf0964a4c7f8e799a946ec01bb463771eb04fc1a323" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lodepng" +version = "3.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a32335d22e44238e2bb0b4d726964d18952ce1f1279ec3305305d2c61539eb" dependencies = [ - "fallible_collections", + "crc32fast", "flate2", "libc", "rgb", @@ -799,78 +1056,76 @@ dependencies = [ [[package]] name = "log" -version = "0.4.14" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lsp-types" -version = "0.80.0" +version = "0.93.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4265e2715bdacbb4dad029fce525e420cd66dc0af24ff9cb996a8ab48ac92ef" +checksum = "9be6e9c7e2d18f651974370d7aff703f9513e0df6e464fd795660edc77e6ca51" dependencies = [ - "base64", - "bitflags", + "bitflags 1.3.2", "serde", "serde_json", "serde_repr", "url", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "matrixmultiply" -version = "0.3.2" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" dependencies = [ + "autocfg", "rawpointer", ] [[package]] -name = "memchr" -version = "2.4.1" +name = "maud" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" dependencies = [ - "autocfg", + "itoa", + "maud_macros", ] [[package]] -name = "miniz_oxide" -version = "0.4.4" +name = "maud_macros" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" dependencies = [ - "adler", - "autocfg", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", ] [[package]] name = "ndarray" -version = "0.15.4" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec23e6762830658d2b3d385a75aa212af2f67a4586d4442907144f3bb6a1ca8" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" dependencies = [ "matrixmultiply", "num-complex", @@ -881,147 +1136,73 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.0" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "once_cell" -version = "1.9.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "opaque-debug" -version = "0.2.3" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "ordered-float" -version = "2.8.0" +version = "3.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c9d06878b3a851e8026ef94bf7fef9ba93062cd412601da4d9cf369b1cc62d" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" dependencies = [ "num-traits", ] -[[package]] -name = "parse-zoneinfo" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" -dependencies = [ - "regex", -] - [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" -version = "0.10.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", - "proc-macro-hack", -] - -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator", - "phf_shared", ] [[package]] name = "phf_generator" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -1029,52 +1210,74 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", - "proc-macro-hack", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] name = "phf_shared" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", - "uncased", ] [[package]] -name = "pkg-config" -version = "0.3.24" +name = "pin-project-lite" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" -version = "0.17.2" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c845088517daa61e8a57eee40309347cea13f273694d1385c553e7a57127763b" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", - "deflate", - "encoding", + "fdeflate", + "flate2", "miniz_oxide", ] [[package]] -name = "ppv-lite86" -version = "0.2.15" +name = "potential_utf" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro-error" @@ -1085,7 +1288,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -1101,27 +1304,44 @@ dependencies = [ ] [[package]] -name = "proc-macro-hack" -version = "0.5.19" +name = "proc-macro-utils" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] [[package]] name = "proc-macro2" -version = "1.0.34" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "version_check", ] [[package]] name = "pulldown-cmark" -version = "0.7.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca36dea94d187597e104a5c8e4b07576a8a45aa5db48a65e12940d3eb7461f55" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags", + "bitflags 2.9.3", "getopts", "memchr", "unicase", @@ -1129,23 +1349,51 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.10" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] -name = "rand" -version = "0.8.4" +name = "quote-use" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "a7b5abe3fe82fdeeb93f44d66a7b444dedf2e4827defb0a8e69c437b2de2ef94" +dependencies = [ + "quote", + "quote-use-macros", + "syn 2.0.106", +] + +[[package]] +name = "quote-use-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ea44c7e20f16017a76a245bb42188517e13d16dcb1aa18044bc406cdc3f4af" +dependencies = [ + "derive-where", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[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", - "rand_hc", ] [[package]] @@ -1160,20 +1408,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", + "getrandom 0.2.16", ] [[package]] @@ -1184,34 +1423,41 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.5.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", - "num_cpus", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -1220,24 +1466,79 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rgb" -version = "0.8.31" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a374af9a0e5fdcdd98c1c7b64f05004f9ea2555b6c75f211daa81268a3c50f1" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" dependencies = [ "bytemuck", ] [[package]] -name = "ryu" -version = "1.0.9" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1248,393 +1549,703 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - [[package]] name = "serde" -version = "1.0.132" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.132" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.73" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.7" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] -name = "sha-1" -version = "0.8.2" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "block-buffer", + "cfg-if", + "cpufeatures", "digest", - "fake-simd", - "opaque-debug", ] +[[package]] +name = "sha256" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" -version = "0.3.7" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] -name = "slug" -version = "0.1.4" +name = "slab" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" -dependencies = [ - "deunicode", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" -version = "0.8.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "structopt" -version = "0.3.25" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "structopt-derive" -version = "0.4.18" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn", + "unicode-ident", ] [[package]] name = "syn" -version = "1.0.82" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] -name = "tera" -version = "1.15.0" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3cac831b615c25bcef632d1cabf864fa05813baad3d526829db18eb70e8b58d" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "chrono", - "chrono-tz", - "globwalk", - "humansize", - "lazy_static", - "percent-encoding", - "pest", - "pest_derive", - "rand", - "regex", - "serde", - "serde_json", - "slug", - "unic-segment", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] name = "termcolor" -version = "1.1.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "tinystr" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ - "unicode-width", + "displaydoc", + "zerovec", ] -[[package]] -name = "thread_local" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" -dependencies = [ - "once_cell", -] - -[[package]] -name = "time" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "tinyvec" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - [[package]] name = "toml" -version = "0.5.8" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] [[package]] name = "typenum" -version = "1.14.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" - -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - -[[package]] -name = "uncased" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" -dependencies = [ - "version_check", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" -dependencies = [ - "unic-ucd-segment", -] - -[[package]] -name = "unic-ucd-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicase" -version = "2.6.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] -name = "unicode-bidi" -version = "0.3.7" +name = "unicode-ident" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" - -[[package]] -name = "unicode-normalization" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] [[package]] name = "url" -version = "2.2.2" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", "serde", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", - "winapi", "winapi-util", ] [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "winapi" -version = "0.3.9" +name = "wasi" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "wit-bindgen", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "wasm-bindgen" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "winapi", + "windows-sys 0.60.2", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-core" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" diff --git a/Cargo.toml b/Cargo.toml index 473daa8a..c5e3616f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "crates/builtins-proc-macro", "crates/dap-types", @@ -12,8 +13,14 @@ members = [ #"crates/spaceman-dmm", ] +[workspace.package] +version = "1.11.0" +authors = ["Tad Hardesty "] +edition = "2021" + [profile.dev] opt-level = 2 [profile.release] lto = true +codegen-units = 1 diff --git a/README.md b/README.md index 7bb5208a..10386c6a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ integration build; see [/tg/station's CI suite][ci] for an example. Support is currently provided in /tg/station13's coderbus (ping `SpaceManiac`) and on the [issue tracker]. Pull requests are welcome. -[DreamMaker]: https://secure.byond.com/ +[DreamMaker]: https://www.byond.com/ [language server]: https://langserver.org/ [releases]: https://github.com/SpaceManiac/SpacemanDMM/releases [ci]: https://github.com/tgstation/tgstation/blob/master/.github/workflows/ci_suite.yml#L45 @@ -93,21 +93,6 @@ the individual packages. [rust]: https://www.rust-lang.org/en-US/install.html [source readme]: ./crates/README.md -### Docker - -A `dockerfile` is provided for the map generator binary. To build the docker -image, enter the SpacemanDMM directory and run: - -```shell -docker build -t spacemandmm . -``` - -To use the image, switch to the codebase you want to generate maps for and invoke the container: - -```shell -docker run -v "$PWD":/usr/src/codebase --rm -it spacemandmm -e /usr/src/codebase/tgstation.dme minimap /usr/src/codebase/_maps/map_files/BoxStation/BoxStation.dmm -``` - ## License SpacemanDMM is free software: you can redistribute it and/or modify diff --git a/crates/builtins-proc-macro/Cargo.toml b/crates/builtins-proc-macro/Cargo.toml index 1c17dcfa..4f9b7b8e 100644 --- a/crates/builtins-proc-macro/Cargo.toml +++ b/crates/builtins-proc-macro/Cargo.toml @@ -2,12 +2,12 @@ name = "builtins-proc-macro" version = "0.0.0" authors = ["Tad Hardesty "] -edition = "2018" +edition = "2021" [lib] proc-macro = true [dependencies] -syn = "1.0" +syn = { version = "1.0", features = ["full"] } quote = "1.0" -proc-macro2 = "1.0.24" +proc-macro2 = "1.0.89" diff --git a/crates/builtins-proc-macro/src/lib.rs b/crates/builtins-proc-macro/src/lib.rs index 0dcec30b..2130d4fb 100644 --- a/crates/builtins-proc-macro/src/lib.rs +++ b/crates/builtins-proc-macro/src/lib.rs @@ -1,16 +1,17 @@ use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::{quote, quote_spanned}; -use syn::*; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use proc_macro2::TokenStream as TokenStream2; +use syn::*; #[derive(Clone, Default)] struct Header { attrs: Vec, path: Vec, + operator_overload_target: Option, } impl Header { @@ -24,7 +25,110 @@ impl Header { input.parse::()?; self.path.push(Ident::parse_any(input)?); } + if let Some(final_ident) = self.path.last() { + // If we find an operator{some token}() pattern we allow the some token part + if final_ident == "operator" { + self.parse_operator(input)?; + } + } + Ok(()) + } + fn parse_operator(&mut self, input: ParseStream) -> Result<()> { + let text_token: Option<&str> = if input.parse::().is_ok() { + if input.parse::().is_ok() { + Some("%%") + } else if input.parse::().is_ok() { + Some("%%=") + } else { + Some("%") + } + } else if input.parse::().is_ok() { + Some("&") + } else if input.parse::().is_ok() { + Some("&=") + } else if input.parse::().is_ok() { + if input.parse::().is_ok() { + Some("**") + } else { + Some("*") + } + } else if input.parse::().is_ok() { + Some("*=") + } else if input.parse::().is_ok() { + Some("/") + } else if input.parse::().is_ok() { + Some("/=") + } else if input.parse::().is_ok() { + if input.parse::().is_ok() { + Some("++") + } else { + Some("+") + } + } else if input.parse::().is_ok() { + Some("+=") + } else if input.parse::().is_ok() { + if input.parse::().is_ok() { + Some("--") + } else { + Some("-") + } + } else if input.parse::().is_ok() { + Some("-=") + } else if input.parse::().is_ok() { + Some("<") + } else if input.parse::().is_ok() { + Some("<<") + } else if input.parse::().is_ok() { + Some("<<=") + } else if input.parse::().is_ok() { + Some("<=") + } else if input.parse::=]>().is_ok() { + Some(">=") + } else if input.parse::>]>().is_ok() { + Some(">>") + } else if input.parse::>=]>().is_ok() { + Some(">>=") + } else if input.parse::().is_ok() { + Some("^") + } else if input.parse::().is_ok() { + Some("^=") + } else if input.parse::().is_ok() { + Some("|") + } else if input.parse::().is_ok() { + Some("|=") + } else if input.parse::().is_ok() { + if input.parse::().is_ok() { + Some("~=") + } else { + Some("~") + } + } else if input.parse::().is_ok() { + Some("~") + } else if input.peek(Token![:]) && input.peek2(Token![=]) { + input.parse::()?; + input.parse::()?; + Some(":=") + } else if self.brackets_next(input).is_ok() { + if input.parse::().is_ok() { + Some("[]=") + } else { + Some("[]") + } + } else { + // Todo: Implement operator""() support. Unsure how to expect an empty string + None + }; + if let Some(text) = text_token { + self.operator_overload_target = Some(text.to_string()); + } + Ok(()) + } + + fn brackets_next(&mut self, input: ParseStream) -> Result<()> { + // Sorry + let _bracket_dummy; + bracketed!(_bracket_dummy in input); Ok(()) } } @@ -48,15 +152,13 @@ impl Parse for ProcArgument { input.parse::()?; input.parse::()?; } - Ok(ProcArgument { - name, - }) + Ok(ProcArgument { name }) } } enum EntryBody { None, - Variable(Option), + Variable(Option>), Proc(Punctuated), } @@ -64,11 +166,13 @@ impl EntryBody { fn parse_with_path(path: &[Ident], input: ParseStream) -> Result { if input.peek(Token![=]) { input.parse::()?; - Ok(EntryBody::Variable(Some(input.parse::()?))) + Ok(EntryBody::Variable(Some(Box::new(input.parse::()?)))) } else if input.peek(syn::token::Paren) { let content; parenthesized!(content in input); - Ok(EntryBody::Proc(content.parse_terminated(ProcArgument::parse)?)) + Ok(EntryBody::Proc( + content.parse_terminated(ProcArgument::parse)?, + )) } else if path.iter().any(|i| i == "var") { Ok(EntryBody::Variable(None)) } else { @@ -87,18 +191,19 @@ impl Parse for BuiltinEntry { let header: Header = input.parse()?; let body = EntryBody::parse_with_path(&header.path, input)?; - input.parse::()?.span; - Ok(BuiltinEntry { - header, - body, - }) + input.parse::()?; + Ok(BuiltinEntry { header, body }) } } struct BuiltinsTable(Vec); impl BuiltinsTable { - fn parse_with_header_into(vec: &mut Vec, header: &Header, input: ParseStream) -> Result<()> { + fn parse_with_header_into( + vec: &mut Vec, + header: &Header, + input: ParseStream, + ) -> Result<()> { while !input.is_empty() { let mut new_header = header.clone(); new_header.parse_mut(input)?; @@ -143,7 +248,19 @@ pub fn builtins_table(input: TokenStream) -> TokenStream { let mut output = Vec::new(); for entry in builtins { let span = entry.header.path.first().unwrap().span(); - let lit_strs: Vec<_> = entry.header.path.into_iter().map(|x| LitStr::new(&x.to_string(), x.span())).collect(); + let mut lit_strs: Vec<_> = entry + .header + .path + .into_iter() + .map(|x| LitStr::new(&x.to_string(), x.span())) + .collect(); + if let Some(operator) = entry.header.operator_overload_target { + let last_entry = lit_strs.pop().unwrap(); + lit_strs.push(LitStr::new( + (last_entry.value() + operator.as_str()).as_str(), + last_entry.span(), + )); + } let path = quote! { &[ #(#lit_strs),* ] }; @@ -159,7 +276,7 @@ pub fn builtins_table(input: TokenStream) -> TokenStream { if ident == "doc" { markdown_span = Some(attr_span); markdown.push_str(&syn::parse2::(attr.tokens).unwrap().0.value()); - markdown.push_str("\n"); + markdown.push('\n'); } else { attr_calls.extend(quote_spanned! { attr_span => .docs.#path }); attr_calls.extend(attr.tokens); @@ -188,14 +305,17 @@ pub fn builtins_table(input: TokenStream) -> TokenStream { } }, EntryBody::Proc(args) => { - let args: Vec<_> = args.into_iter().map(|x| LitStr::new(&x.name.to_string(), x.name.span())).collect(); + let args: Vec<_> = args + .into_iter() + .map(|x| LitStr::new(&x.name.to_string(), x.name.span())) + .collect(); quote_spanned! { span => tree.add_builtin_proc(#path, &[ #(#args),* ]) #attr_calls; } - } + }, }; output.push(line); } - output.into_iter().flat_map(|x| TokenStream::from(x)).collect() + output.into_iter().flat_map(TokenStream::from).collect() } diff --git a/crates/dap-types/Cargo.toml b/crates/dap-types/Cargo.toml index 67f0e7ae..41ed3fa1 100644 --- a/crates/dap-types/Cargo.toml +++ b/crates/dap-types/Cargo.toml @@ -2,10 +2,10 @@ name = "dap-types" version = "0.0.0" authors = ["Tad Hardesty "] -edition = "2018" +edition = "2021" [dependencies] -serde = "1.0.27" -serde_json = "1.0.10" -serde_derive = "1.0.27" -ahash = "0.7.6" +serde = "1.0.213" +serde_json = "1.0.132" +serde_derive = "1.0.213" +foldhash = "0.2.0" diff --git a/crates/dap-types/src/lib.rs b/crates/dap-types/src/lib.rs index 33d9f610..303b05e4 100644 --- a/crates/dap-types/src/lib.rs +++ b/crates/dap-types/src/lib.rs @@ -4,10 +4,9 @@ #![deny(unsafe_code)] #![allow(non_snake_case)] -use std::collections::HashMap; +use foldhash::HashMap; +use serde_derive::{Deserialize, Serialize}; use serde_json::Value; -use serde_derive::{Serialize, Deserialize}; -use ahash::RandomState; pub trait Request { type Params; @@ -176,8 +175,8 @@ pub struct ThreadEvent { pub reason: String, /** - * The identifier of the thread. - */ + * The identifier of the thread. + */ pub threadId: i64, } @@ -578,7 +577,7 @@ pub struct EvaluateResponse { /** * The optional type of the evaluate result. */ - #[serde(rename="type")] + #[serde(rename = "type")] pub type_: Option, /** @@ -611,13 +610,19 @@ pub struct EvaluateResponse { impl From for EvaluateResponse { fn from(result: String) -> EvaluateResponse { - EvaluateResponse { result, .. Default::default() } + EvaluateResponse { + result, + ..Default::default() + } } } impl From<&str> for EvaluateResponse { fn from(result: &str) -> EvaluateResponse { - EvaluateResponse { result: result.to_owned(), .. Default::default() } + EvaluateResponse { + result: result.to_owned(), + ..Default::default() + } } } @@ -929,7 +934,10 @@ pub struct SourceResponse { impl From for SourceResponse { fn from(content: String) -> SourceResponse { - SourceResponse { content, mimeType: None } + SourceResponse { + content, + mimeType: None, + } } } @@ -1078,9 +1086,9 @@ pub struct VariablesArguments { #[derive(Serialize, Deserialize, Debug)] pub enum VariablesFilter { - #[serde(rename="indexed")] + #[serde(rename = "indexed")] Indexed, - #[serde(rename="named")] + #[serde(rename = "named")] Named, } @@ -1359,16 +1367,16 @@ pub struct DisassembledInstruction { #[derive(Serialize, Deserialize, Debug)] pub enum ExceptionBreakMode { /// never breaks - #[serde(rename="never")] + #[serde(rename = "never")] Never, /// always breaks - #[serde(rename="always")] + #[serde(rename = "always")] Always, /// breaks when exception unhandled - #[serde(rename="unhandled")] + #[serde(rename = "unhandled")] Unhandled, /// breaks if the exception is not handled by user code - #[serde(rename="userUnhandled")] + #[serde(rename = "userUnhandled")] UserUnhandled, } @@ -1530,7 +1538,7 @@ pub struct Message { /** * An object used as a dictionary for looking up the variables in the format string. */ - pub variables: Option>, + pub variables: Option>, /** * If true send to telemetry. @@ -1661,7 +1669,6 @@ pub struct Source { * Optional data that a debug adapter might want to loop through the client. The client should leave the data intact and persist it across sessions. The client should not interpret the data. */ pub adapterData: Option, - /*/** * The checksums associated with this file. */ @@ -1670,11 +1677,11 @@ pub struct Source { #[derive(Serialize, Deserialize, Debug)] pub enum SourcePresentationHint { - #[serde(rename="normal")] + #[serde(rename = "normal")] Normal, - #[serde(rename="emphasize")] + #[serde(rename = "emphasize")] Emphasize, - #[serde(rename="deemphasize")] + #[serde(rename = "deemphasize")] Deemphasize, } @@ -1754,7 +1761,6 @@ pub struct StackFrame { * The module associated with this frame, if any. */ moduleId?: number | string;*/ - /** * An optional hint for how to present this frame in the UI. A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. */ @@ -1763,11 +1769,11 @@ pub struct StackFrame { #[derive(Serialize, Deserialize, Debug)] pub enum StackFramePresentationHint { - #[serde(rename="normal")] + #[serde(rename = "normal")] Normal, - #[serde(rename="label")] + #[serde(rename = "label")] Label, - #[serde(rename="subtle")] + #[serde(rename = "subtle")] Subtle, } @@ -1871,7 +1877,7 @@ pub struct Variable { /** * The type of the variable's value. Typically shown in the UI when hovering over the value. */ - #[serde(rename="type")] + #[serde(rename = "type")] pub type_: Option, /** diff --git a/crates/dm-langserver/Cargo.toml b/crates/dm-langserver/Cargo.toml index 2336c4e1..d2010f7e 100644 --- a/crates/dm-langserver/Cargo.toml +++ b/crates/dm-langserver/Cargo.toml @@ -1,27 +1,31 @@ [package] name = "dm-langserver" -version = "1.5.1" -authors = ["Tad Hardesty "] -edition = "2018" +version.workspace = true +authors.workspace = true +edition.workspace = true [dependencies] -url = "2.1.0" -serde = "1.0.27" -serde_json = "1.0.10" -serde_derive = "1.0.27" -bincode = "1.3.1" -jsonrpc-core = "14.0.3" -lsp-types = "0.80.0" +url = "2.5.2" +serde = "1.0.213" +serde_json = "1.0.132" +serde_derive = "1.0.213" +bincode = "1.3.3" +jsonrpc-core = "18.0.0" +lsp-types = "0.93.2" dap-types = { path = "../dap-types" } dreammaker = { path = "../dreammaker" } dreamchecker = { path = "../dreamchecker" } interval-tree = { path = "../interval-tree" } -libc = "0.2.65" -guard = "0.5.0" -regex = "1.3" -lazy_static = "1.4" -ahash = "0.7.6" +libc = "0.2.161" +regex = "1.11.1" +lazy_static = "1.5" +foldhash = "0.2.0" [build-dependencies] -chrono = "0.4.0" -git2 = { version = "0.13", default-features = false } +chrono = "0.4.38" +git2 = { version = "0.20.2", default-features = false } +sha256 = { version = "1.5.0", default-features = false } +ureq = "2.10.1" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(auxtools_bundle)', 'cfg(extools_bundle)'] } diff --git a/crates/dm-langserver/README.md b/crates/dm-langserver/README.md index 52ba590b..9835e38e 100644 --- a/crates/dm-langserver/README.md +++ b/crates/dm-langserver/README.md @@ -12,7 +12,7 @@ client support. compatible. [language server]: https://langserver.org/ -[BYOND]: https://secure.byond.com/ +[BYOND]: https://www.byond.com/ ## Code completion diff --git a/crates/dm-langserver/build.rs b/crates/dm-langserver/build.rs index 91478f6e..456a4c20 100644 --- a/crates/dm-langserver/build.rs +++ b/crates/dm-langserver/build.rs @@ -1,33 +1,41 @@ extern crate chrono; extern crate git2; -use std::io::Write; -use std::fs::File; use std::env; -use std::path::PathBuf; +use std::fs::File; +use std::io::Write; +use std::path::{Path, PathBuf}; fn main() { // build info let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - let mut f = File::create(&out_dir.join("build-info.txt")).unwrap(); + let mut f = File::create(out_dir.join("build-info.txt")).unwrap(); match read_commit() { - Ok(commit) => writeln!(f, "commit: {}", commit).unwrap(), - Err(err) => println!("cargo:warning=Failed to fetch commit info: {}", err) + Ok(commit) => writeln!(f, "commit: {commit}").unwrap(), + Err(err) => println!("cargo:warning=Failed to fetch commit info: {err}"), } - writeln!(f, "build date: {}", chrono::Utc::today()).unwrap(); + writeln!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap(); // extools bundling - println!("cargo:rerun-if-env-changed=EXTOOLS_BUNDLE_DLL"); - if env::var_os("EXTOOLS_BUNDLE_DLL").is_some() { - println!("cargo:rustc-cfg=extools_bundle"); - } + println!("cargo:rustc-cfg=extools_bundle"); + download_dll( + &out_dir, + "extools.dll", + "v0.0.7", // EXTOOLS_TAG + "https://github.com/tgstation/tgstation/raw/34f0cc6394a064b87cbd1d6cb225f1d3df444ba7/byond-extools.dll", // EXTOOLS_DLL_URL + "073dd08790a13580bae71758e9217917700dd85ce8d35cb030cef0cf5920fca8", // EXTOOLS_DLL_SHA256 + ); // auxtools bundling - println!("cargo:rerun-if-env-changed=AUXTOOLS_BUNDLE_DLL"); - if env::var_os("AUXTOOLS_BUNDLE_DLL").is_some() { - println!("cargo:rustc-cfg=auxtools_bundle"); - } + println!("cargo:rustc-cfg=auxtools_bundle"); + download_dll( + &out_dir, + "debug_server.dll", + "v2.3.5", // DEBUG_SERVER_TAG + "https://github.com/willox/auxtools/releases/download/v2.3.5/debug_server.dll", // DEBUG_SERVER_DLL_URL + "dfcaa1086608047559103b55396f99504320f2b0ec1695baa3dc34dbd41695b2", // DEBUG_SERVER_DLL_SHA256 + ); } fn read_commit() -> Result { @@ -35,28 +43,64 @@ fn read_commit() -> Result { let head = repo.head()?.peel_to_commit()?.id(); let mut all_tags = Vec::new(); - repo.tag_foreach(|oid, _| { all_tags.push(oid); true })?; + repo.tag_foreach(|oid, _| { + all_tags.push(oid); + true + })?; let mut best = None; for tag_id in all_tags { - let tag_commit = repo.find_tag(tag_id)?.as_object().peel_to_commit()?.id(); - let (ahead, behind) = repo.graph_ahead_behind(head, tag_commit)?; - if behind == 0 { - match best { - None => best = Some(ahead), - Some(prev) if ahead < prev => best = Some(ahead), - _ => {} + if let Ok(possible_tag) = repo.find_tag(tag_id) { + let tag_commit = possible_tag.as_object().peel_to_commit()?.id(); + let (ahead, behind) = repo.graph_ahead_behind(head, tag_commit)?; + if behind == 0 { + match best { + None => best = Some(ahead), + Some(prev) if ahead < prev => best = Some(ahead), + _ => {}, + } + } + if ahead == 0 { + break; } - } - if ahead == 0 { - break; } } match best { - None | Some(0) => {} - Some(ahead) => println!("cargo:rustc-env=CARGO_PKG_VERSION={}+{}", std::env::var("CARGO_PKG_VERSION").unwrap(), ahead), + None | Some(0) => {}, + Some(ahead) => println!( + "cargo:rustc-env=CARGO_PKG_VERSION={}+{}", + std::env::var("CARGO_PKG_VERSION").unwrap(), + ahead + ), } Ok(head.to_string()) } + +fn download_dll(out_dir: &Path, fname: &str, tag: &str, url: &str, sha256: &str) { + let full_path = out_dir.join(fname); + println!( + "cargo:rustc-env=BUNDLE_PATH_{}={}", + fname, + full_path.display() + ); + println!("cargo:rustc-env=BUNDLE_VERSION_{fname}={tag}"); + + if let Ok(digest) = sha256::try_digest(&full_path) { + if digest == sha256 { + return; + } + } + + std::io::copy( + &mut ureq::get(url) + .call() + .expect("Error downloading DLL to bundle") + .into_reader(), + &mut std::fs::File::create(&full_path).unwrap(), + ) + .unwrap(); + + assert_eq!(sha256, sha256::try_digest(&full_path).unwrap()); +} diff --git a/crates/dm-langserver/src/background.rs b/crates/dm-langserver/src/background.rs index db04e008..18085aef 100644 --- a/crates/dm-langserver/src/background.rs +++ b/crates/dm-langserver/src/background.rs @@ -34,9 +34,9 @@ impl Background { match rx.try_recv() { Ok(v) => { self.value = Some(v); - } + }, Err(TryRecvError::Empty) => self.rx = Some(rx), - Err(TryRecvError::Disconnected) => {} + Err(TryRecvError::Disconnected) => {}, } } self diff --git a/crates/dm-langserver/src/color.rs b/crates/dm-langserver/src/color.rs index 5872190c..bad2f899 100644 --- a/crates/dm-langserver/src/color.rs +++ b/crates/dm-langserver/src/color.rs @@ -7,7 +7,7 @@ use regex::Regex; /// Extract ranges and colors from an input string. -pub fn extract_colors<'a>(input: &'a str) -> impl Iterator + 'a { +pub fn extract_colors(input: &str) -> impl Iterator + '_ { COLOR_REGEX.captures_iter(input).flat_map(|capture| { parse_capture(&capture).map(|rgba| { let totality = capture.get(0).unwrap(); @@ -26,7 +26,7 @@ pub enum ColorFormat { }, Rgb { alpha: bool, - } + }, } impl ColorFormat { @@ -53,26 +53,45 @@ impl ColorFormat { pub fn format(self, [r, g, b, a]: [u8; 4]) -> String { match self { - ColorFormat::Hex { single_quoted, short, alpha } => { + ColorFormat::Hex { + single_quoted, + short, + alpha, + } => { let q = if single_quoted { '\'' } else { '"' }; - let short = short && r % 0x11 == 0 && g % 0x11 == 0 && b % 0x11 == 0 && a % 0x11 == 0; + let short = + short && r % 0x11 == 0 && g % 0x11 == 0 && b % 0x11 == 0 && a % 0x11 == 0; let alpha = alpha || a != 255; match (short, alpha) { - (false, false) => format!("{}#{:02x}{:02x}{:02x}{}", q, r, g, b, q), - (false, true) => format!("{}#{:02x}{:02x}{:02x}{:02x}{}", q, r, g, b, a, q), - (true, false) => format!("{}#{:x}{:x}{:x}{}", q, r / 0x11, g / 0x11, b / 0x11, q), - (true, true) => format!("{}#{:x}{:x}{:x}{:x}{}", q, r / 0x11, g / 0x11, b / 0x11, a / 0x11, q), + (false, false) => format!("{q}#{r:02x}{g:02x}{b:02x}{q}"), + (false, true) => format!("{q}#{r:02x}{g:02x}{b:02x}{a:02x}{q}"), + (true, false) => { + format!("{}#{:x}{:x}{:x}{}", q, r / 0x11, g / 0x11, b / 0x11, q) + }, + (true, true) => format!( + "{}#{:x}{:x}{:x}{:x}{}", + q, + r / 0x11, + g / 0x11, + b / 0x11, + a / 0x11, + q + ), } }, - ColorFormat::Rgb { alpha } if alpha || a != 255 => format!("rgb({}, {}, {}, {})", r, g, b, a), - ColorFormat::Rgb { alpha: _ } => format!("rgb({}, {}, {})", r, g, b), + ColorFormat::Rgb { alpha } if alpha || a != 255 => format!("rgb({r}, {g}, {b}, {a})"), + ColorFormat::Rgb { alpha: _ } => format!("rgb({r}, {g}, {b})"), } } } impl Default for ColorFormat { fn default() -> ColorFormat { - ColorFormat::Hex { single_quoted: false, short: false, alpha: false } + ColorFormat::Hex { + single_quoted: false, + short: false, + alpha: false, + } } } @@ -83,11 +102,19 @@ lazy_static! { fn parse_capture(capture: ®ex::Captures) -> Option<[u8; 4]> { // Tied closely to the regex above. - match (capture.get(1), capture.get(2), capture.get(3), capture.get(4), capture.get(5), capture.get(6)) { - (Some(cap), _, _, _, _, _) | - (_, Some(cap), _, _, _, _) => parse_hex(cap.as_str()), - (_, _, Some(r), Some(g), Some(b), a) => parse_rgba(r.as_str(), g.as_str(), b.as_str(), a.map(|a| a.as_str())), - _ => None + match ( + capture.get(1), + capture.get(2), + capture.get(3), + capture.get(4), + capture.get(5), + capture.get(6), + ) { + (Some(cap), _, _, _, _, _) | (_, Some(cap), _, _, _, _) => parse_hex(cap.as_str()), + (_, _, Some(r), Some(g), Some(b), a) => { + parse_rgba(r.as_str(), g.as_str(), b.as_str(), a.map(|a| a.as_str())) + }, + _ => None, } } @@ -97,28 +124,27 @@ fn parse_hex(hex: &str) -> Option<[u8; 4]> { sum = 16 * sum + ch.to_digit(16).unwrap_or(0); } - if hex.len() == 8 { // #rrggbbaa + if hex.len() == 8 { + // #rrggbbaa Some([ (sum >> 24) as u8, (sum >> 16) as u8, (sum >> 8) as u8, sum as u8, ]) - } else if hex.len() == 6 { // #rrggbb - Some([ - (sum >> 16) as u8, - (sum >> 8) as u8, - sum as u8, - 255, - ]) - } else if hex.len() == 4 { // #rgba + } else if hex.len() == 6 { + // #rrggbb + Some([(sum >> 16) as u8, (sum >> 8) as u8, sum as u8, 255]) + } else if hex.len() == 4 { + // #rgba Some([ (0x11 * ((sum >> 12) & 0xf)) as u8, (0x11 * ((sum >> 8) & 0xf)) as u8, (0x11 * ((sum >> 4) & 0xf)) as u8, (0x11 * (sum & 0xf)) as u8, ]) - } else if hex.len() == 3 { // #rgb + } else if hex.len() == 3 { + // #rgb Some([ (0x11 * ((sum >> 8) & 0xf)) as u8, (0x11 * ((sum >> 4) & 0xf)) as u8, diff --git a/crates/dm-langserver/src/completion.rs b/crates/dm-langserver/src/completion.rs index 71f4add8..15e3713c 100644 --- a/crates/dm-langserver/src/completion.rs +++ b/crates/dm-langserver/src/completion.rs @@ -1,24 +1,25 @@ //! Supporting functions for completion and go-to-definition. -use std::collections::HashSet; +use foldhash::{HashSet, HashSetExt}; use lsp_types::*; -use dm::ast::PathOp; use dm::annotation::Annotation; -use dm::objtree::{TypeRef, TypeVar, TypeProc, ProcValue}; +use dm::ast::PathOp; +use dm::objtree::{ProcValue, TypeProc, TypeRef, TypeVar}; -use crate::{Engine, Span, is_constructor_name}; use crate::symbol_search::contains; +use crate::{is_constructor_name, Engine, Span}; -use ahash::RandomState; - +#[rustfmt::skip] static PROC_KEYWORDS: &[&str] = &[ // Implicit variables "args", "global", "src", "usr", + "caller", + "callee", // Term "null", @@ -58,7 +59,7 @@ fn item_var(ty: TypeRef, name: &str, var: &TypeVar) -> CompletionItem { if ty.is_root() { detail = constant.to_string(); } else { - detail = format!("{} - {}", constant, detail); + detail = format!("{constant} - {detail}"); } } } @@ -66,10 +67,10 @@ fn item_var(ty: TypeRef, name: &str, var: &TypeVar) -> CompletionItem { CompletionItem { label: name.to_owned(), - kind: Some(CompletionItemKind::Field), + kind: Some(CompletionItemKind::FIELD), detail: Some(detail), documentation: item_documentation(&var.value.docs), - .. Default::default() + ..Default::default() } } @@ -77,11 +78,11 @@ fn item_proc(ty: TypeRef, name: &str, proc: &TypeProc) -> CompletionItem { CompletionItem { label: name.to_owned(), kind: Some(if ty.is_root() { - CompletionItemKind::Function + CompletionItemKind::FUNCTION } else if is_constructor_name(name) { - CompletionItemKind::Constructor + CompletionItemKind::CONSTRUCTOR } else { - CompletionItemKind::Method + CompletionItemKind::METHOD }), detail: Some(format!("on {}", ty.pretty_path())), documentation: item_documentation(&proc.main_value().docs), @@ -102,7 +103,7 @@ fn item_documentation(docs: &dm::docs::DocCollection) -> Option { fn items_ty<'a>( results: &mut Vec, - skip: &mut HashSet<(&str, &'a String), RandomState>, + skip: &mut HashSet<(&str, &'a String)>, ty: TypeRef<'a>, query: &str, ) { @@ -124,14 +125,19 @@ fn items_ty<'a>( if contains(name, query) { results.push(CompletionItem { insert_text: Some(name.to_owned()), - .. item_proc(ty, name, proc) + ..item_proc(ty, name, proc) }); } } } -pub fn combine_tree_path<'a, I>(iter: &I, mut absolute: bool, mut parts: &'a [String]) -> impl Iterator - where I: Iterator + Clone +pub fn combine_tree_path<'a, I>( + iter: &I, + mut absolute: bool, + mut parts: &'a [String], +) -> impl Iterator +where + I: Iterator + Clone, { // cut off the part of the path we haven't selected if_annotation! { Annotation::InSequence(idx) in iter; { @@ -165,10 +171,14 @@ pub fn combine_tree_path<'a, I>(iter: &I, mut absolute: bool, mut parts: &'a [St prefix_parts.iter().chain(parts).map(|x| &**x) } -impl<'a> Engine<'a> { - pub fn follow_type_path<'b, I>(&'b self, iter: &I, mut parts: &'b [(PathOp, String)]) -> Option> +impl Engine { + pub fn follow_type_path<'b, I>( + &'b self, + iter: &I, + mut parts: &'b [(PathOp, String)], + ) -> Option> where - I: Iterator + Clone, + I: Iterator + Clone, { // cut off the part of the path we haven't selected if_annotation! { Annotation::InSequence(idx) in iter; { @@ -177,7 +187,7 @@ impl<'a> Engine<'a> { // if we're on the right side of a 'list/', start the lookup there match parts.split_first() { Some(((PathOp::Slash, kwd), rest)) if kwd == "list" && !rest.is_empty() => parts = rest, - _ => {} + _ => {}, } // use the first path op to select the starting type of the lookup @@ -189,7 +199,7 @@ impl<'a> Engine<'a> { }); } let mut ty = match parts[0].0 { - PathOp::Colon => return None, // never finds anything, apparently? + PathOp::Colon => return None, // never finds anything, apparently? PathOp::Slash => self.objtree.root(), PathOp::Dot => match self.find_type_context(iter) { (Some(base), _) => base, @@ -200,7 +210,7 @@ impl<'a> Engine<'a> { // follow the path ops until we hit 'proc' or 'verb' let mut iter = parts.iter(); let mut decl = None; - while let Some(&(op, ref name)) = iter.next() { + for &(op, ref name) in iter.by_ref() { if name == "proc" { decl = Some("proc"); break; @@ -228,14 +238,20 @@ impl<'a> Engine<'a> { Some(TypePathResult { ty, decl, proc }) } - pub fn tree_completions(&self, results: &mut Vec, exact: bool, ty: TypeRef, query: &str) { + pub fn tree_completions( + &self, + results: &mut Vec, + exact: bool, + ty: TypeRef, + query: &str, + ) { // path keywords for &name in ["proc", "var", "verb"].iter() { if contains(name, query) { results.push(CompletionItem { label: name.to_owned(), - kind: Some(CompletionItemKind::Keyword), - .. Default::default() + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() }) } } @@ -246,16 +262,16 @@ impl<'a> Engine<'a> { if contains(child.name(), query) { results.push(CompletionItem { label: child.name().to_owned(), - kind: Some(CompletionItemKind::Class), + kind: Some(CompletionItemKind::CLASS), documentation: item_documentation(&child.docs), - .. Default::default() + ..Default::default() }); } } } let mut next = Some(ty).filter(|ty| !ty.is_root()); - let mut skip = HashSet::with_hasher(RandomState::default()); + let mut skip = HashSet::new(); while let Some(ty) = next { // override a parent's var for (name, var) in ty.get().vars.iter() { @@ -264,8 +280,8 @@ impl<'a> Engine<'a> { } if contains(name, query) { results.push(CompletionItem { - insert_text: Some(format!("{} = ", name)), - .. item_var(ty, name, var) + insert_text: Some(format!("{name} = ")), + ..item_var(ty, name, var) }); } } @@ -278,11 +294,11 @@ impl<'a> Engine<'a> { if contains(name, query) { use std::fmt::Write; - let mut completion = format!("{}(", name); + let mut completion = format!("{name}("); let mut sep = ""; for param in proc.main_value().parameters.iter() { for each in param.var_type.type_path.iter() { - let _ = write!(completion, "{}{}", sep, each); + let _ = write!(completion, "{sep}{each}"); sep = "/"; } let _ = write!(completion, "{}{}", sep, param.name); @@ -292,7 +308,7 @@ impl<'a> Engine<'a> { results.push(CompletionItem { insert_text: Some(completion), - .. item_proc(ty, name, proc) + ..item_proc(ty, name, proc) }); } } @@ -323,8 +339,8 @@ impl<'a> Engine<'a> { if contains(name, query) { results.push(CompletionItem { label: name.to_owned(), - kind: Some(CompletionItemKind::Keyword), - .. Default::default() + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() }) } } @@ -334,9 +350,9 @@ impl<'a> Engine<'a> { if contains(child.name(), query) { results.push(CompletionItem { label: child.name().to_owned(), - kind: Some(CompletionItemKind::Class), + kind: Some(CompletionItemKind::CLASS), documentation: item_documentation(&child.docs), - .. Default::default() + ..Default::default() }); } } @@ -349,7 +365,7 @@ impl<'a> Engine<'a> { proc: None, }) => { let mut next = Some(ty); - let mut skip = HashSet::with_hasher(RandomState::default()); + let mut skip = HashSet::new(); while let Some(ty) = next { // reference a declared proc for (name, proc) in ty.get().procs.iter() { @@ -371,12 +387,16 @@ impl<'a> Engine<'a> { next = ty.parent_type_without_root(); } }, - _ => {} + _ => {}, } } - pub fn unscoped_completions<'b, I>(&'b self, results: &mut Vec, iter: &I, query: &str) - where + pub fn unscoped_completions<'b, I>( + &'b self, + results: &mut Vec, + iter: &I, + query: &str, + ) where I: Iterator + Clone, { let (ty, proc_name) = self.find_type_context(iter); @@ -387,8 +407,8 @@ impl<'a> Engine<'a> { if contains(name, query) { results.push(CompletionItem { label: name.to_owned(), - kind: Some(CompletionItemKind::Keyword), - .. Default::default() + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() }); } } @@ -400,16 +420,16 @@ impl<'a> Engine<'a> { if contains(name, query) { results.push(CompletionItem { label: name.clone(), - kind: Some(CompletionItemKind::Variable), + kind: Some(CompletionItemKind::VARIABLE), detail: Some("(local)".to_owned()), - .. Default::default() + ..Default::default() }); } } } // proc parameters - let ty = ty.unwrap_or(self.objtree.root()); + let ty = ty.unwrap_or_else(|| self.objtree.root()); if let Some((proc_name, idx)) = proc_name { if let Some(proc) = ty.get().procs.get(proc_name) { if let Some(value) = proc.value.get(idx) { @@ -417,9 +437,9 @@ impl<'a> Engine<'a> { if contains(¶m.name, query) { results.push(CompletionItem { label: param.name.clone(), - kind: Some(CompletionItemKind::Variable), + kind: Some(CompletionItemKind::VARIABLE), detail: Some("(parameter)".to_owned()), - .. Default::default() + ..Default::default() }); } } @@ -430,14 +450,14 @@ impl<'a> Engine<'a> { // macros if let Some(ref defines) = self.defines { // TODO: verify that the macro is in scope at the location - for (_, &(ref name, ref define)) in defines.iter() { + for (_, (name, define)) in defines.iter() { if contains(name, query) { results.push(CompletionItem { label: name.to_owned(), - kind: Some(CompletionItemKind::Constant), + kind: Some(CompletionItemKind::CONSTANT), detail: Some(define.display_with_name(name).to_string()), documentation: item_documentation(define.docs()), - .. Default::default() + ..Default::default() }); } } @@ -445,7 +465,7 @@ impl<'a> Engine<'a> { // fields let mut next = Some(ty); - let mut skip = HashSet::with_hasher(RandomState::default()); + let mut skip = HashSet::new(); while let Some(ty) = next { items_ty(results, &mut skip, ty, query); next = ty.parent_type(); @@ -462,7 +482,7 @@ impl<'a> Engine<'a> { I: Iterator + Clone, { let mut next = self.find_scoped_type(iter, priors); - let mut skip = HashSet::with_hasher(RandomState::default()); + let mut skip = HashSet::new(); while let Some(ty) = next { items_ty(results, &mut skip, ty, query); next = ty.parent_type_without_root(); diff --git a/crates/dm-langserver/src/debugger/auxtools.rs b/crates/dm-langserver/src/debugger/auxtools.rs index 18486318..4babce9a 100644 --- a/crates/dm-langserver/src/debugger/auxtools.rs +++ b/crates/dm-langserver/src/debugger/auxtools.rs @@ -1,5 +1,4 @@ use super::auxtools_types::*; -use std::{net::TcpListener, sync::mpsc}; use std::thread; use std::{ io::{Read, Write}, @@ -9,6 +8,7 @@ use std::{ sync::{Arc, RwLock}, thread::JoinHandle, }; +use std::{net::TcpListener, sync::mpsc}; use super::SequenceNumber; @@ -23,6 +23,7 @@ enum StreamState { } pub struct Auxtools { + #[allow(dead_code)] seq: Arc, responses: mpsc::Receiver, _thread: JoinHandle<()>, @@ -36,6 +37,12 @@ pub struct AuxtoolsThread { last_error: Arc>, } +pub struct AuxtoolsScopes { + pub arguments: Option, + pub locals: Option, + pub globals: Option, +} + impl Auxtools { pub fn connect(seq: Arc, port: Option) -> std::io::Result { let addr: SocketAddr = (Ipv4Addr::LOCALHOST, port.unwrap_or(DEFAULT_PORT)).into(); @@ -51,8 +58,9 @@ impl Auxtools { AuxtoolsThread { seq, responses: responses_sender, - last_error: last_error, - }.run(stream); + last_error, + } + .run(stream); }) }; @@ -79,26 +87,35 @@ impl Auxtools { AuxtoolsThread { seq, responses: responses_sender, - last_error: last_error, - }.spawn_listener(listener, connection_sender) + last_error, + } + .spawn_listener(listener, connection_sender) }; - Ok((port, Auxtools { - seq, - responses: responses_receiver, - _thread: thread, - stream: StreamState::Waiting(connection_receiver), - last_error, - })) + Ok(( + port, + Auxtools { + seq, + responses: responses_receiver, + _thread: thread, + stream: StreamState::Waiting(connection_receiver), + last_error, + }, + )) } fn read_response_or_disconnect(&mut self) -> Result> { - match self.responses.recv_timeout(std::time::Duration::from_secs(5)) { + match self + .responses + .recv_timeout(std::time::Duration::from_secs(5)) + { Ok(response) => Ok(response), Err(_) => { self.disconnect(); - Err(Box::new(super::GenericError("timed out waiting for response"))) - } + Err(Box::new(super::GenericError( + "timed out waiting for response", + ))) + }, } } @@ -116,12 +133,12 @@ impl Auxtools { stream.write_all(&data[..])?; stream.flush()?; Ok(()) - } + }, _ => { // Success if not connected (kinda dumb) Ok(()) - } + }, } } @@ -150,7 +167,7 @@ impl Auxtools { self.send_or_disconnect(Request::Configured)?; match self.read_response_or_disconnect()? { - Response::Ack { .. } => Ok(()), + Response::Ack => Ok(()), response => Err(Box::new(UnexpectedResponse::new("Ack", response))), } } @@ -164,8 +181,13 @@ impl Auxtools { } } - pub fn eval(&mut self, frame_id: Option, command: &str, context: Option) -> Result> { - self.send_or_disconnect(Request::Eval{ + pub fn eval( + &mut self, + frame_id: Option, + command: &str, + context: Option, + ) -> Result> { + self.send_or_disconnect(Request::Eval { frame_id, command: command.to_owned(), context, @@ -177,16 +199,29 @@ impl Auxtools { } } - pub fn get_current_proc(&mut self, frame_id: u32) -> Result, Box> { + #[allow(dead_code)] + pub fn get_current_proc( + &mut self, + frame_id: u32, + ) -> Result, Box> { self.send_or_disconnect(Request::CurrentInstruction { frame_id })?; match self.read_response_or_disconnect()? { Response::CurrentInstruction(ins) => Ok(ins.map(|x| (x.proc.path, x.proc.override_id))), - response => Err(Box::new(UnexpectedResponse::new("CurrentInstruction", response))), + response => Err(Box::new(UnexpectedResponse::new( + "CurrentInstruction", + response, + ))), } } - pub fn get_line_number(&mut self, path: &str, override_id: u32, offset: u32) -> Result, Box> { + #[allow(dead_code)] + pub fn get_line_number( + &mut self, + path: &str, + override_id: u32, + offset: u32, + ) -> Result, Box> { self.send_or_disconnect(Request::LineNumber { proc: ProcRef { path: path.to_owned(), @@ -201,7 +236,12 @@ impl Auxtools { } } - pub fn get_offset(&mut self, path: &str, override_id: u32, line: u32) -> Result, Box> { + pub fn get_offset( + &mut self, + path: &str, + override_id: u32, + line: u32, + ) -> Result, Box> { self.send_or_disconnect(Request::Offset { proc: ProcRef { path: path.to_owned(), @@ -216,10 +256,14 @@ impl Auxtools { } } - pub fn set_breakpoint(&mut self, instruction: InstructionRef, condition: Option) -> Result> { + pub fn set_breakpoint( + &mut self, + instruction: InstructionRef, + condition: Option, + ) -> Result> { self.send_or_disconnect(Request::BreakpointSet { instruction, - condition + condition, })?; match self.read_response_or_disconnect()? { @@ -228,14 +272,20 @@ impl Auxtools { } } - pub fn unset_breakpoint(&mut self, instruction: &InstructionRef) -> Result<(), Box> { + pub fn unset_breakpoint( + &mut self, + instruction: &InstructionRef, + ) -> Result<(), Box> { self.send_or_disconnect(Request::BreakpointUnset { instruction: instruction.clone(), })?; match self.read_response_or_disconnect()? { Response::BreakpointUnset { .. } => Ok(()), - response => Err(Box::new(UnexpectedResponse::new("BreakpointUnset", response))), + response => Err(Box::new(UnexpectedResponse::new( + "BreakpointUnset", + response, + ))), } } @@ -245,7 +295,7 @@ impl Auxtools { })?; match self.read_response_or_disconnect()? { - Response::Ack { .. } => Ok(()), + Response::Ack => Ok(()), response => Err(Box::new(UnexpectedResponse::new("Ack", response))), } } @@ -256,7 +306,7 @@ impl Auxtools { })?; match self.read_response_or_disconnect()? { - Response::Ack { .. } => Ok(()), + Response::Ack => Ok(()), response => Err(Box::new(UnexpectedResponse::new("Ack", response))), } } @@ -267,7 +317,7 @@ impl Auxtools { })?; match self.read_response_or_disconnect()? { - Response::Ack { .. } => Ok(()), + Response::Ack => Ok(()), response => Err(Box::new(UnexpectedResponse::new("Ack", response))), } } @@ -278,7 +328,7 @@ impl Auxtools { })?; match self.read_response_or_disconnect()? { - Response::Ack { .. } => Ok(()), + Response::Ack => Ok(()), response => Err(Box::new(UnexpectedResponse::new("Ack", response))), } } @@ -287,7 +337,7 @@ impl Auxtools { self.send_or_disconnect(Request::Pause)?; match self.read_response_or_disconnect()? { - Response::Ack { .. } => Ok(()), + Response::Ack => Ok(()), response => Err(Box::new(UnexpectedResponse::new("Ack", response))), } } @@ -323,25 +373,31 @@ impl Auxtools { } // TODO: return all the scopes - pub fn get_scopes(&mut self, frame_id: u32) -> Result<(Option, Option, Option), Box> { - self.send_or_disconnect(Request::Scopes { - frame_id - })?; + pub fn get_scopes( + &mut self, + frame_id: u32, + ) -> Result> { + self.send_or_disconnect(Request::Scopes { frame_id })?; match self.read_response_or_disconnect()? { Response::Scopes { arguments, locals, globals, - } => Ok((arguments, locals, globals)), + } => Ok(AuxtoolsScopes { + arguments, + locals, + globals, + }), response => Err(Box::new(UnexpectedResponse::new("Scopes", response))), } } - pub fn get_variables(&mut self, vars: VariablesRef) -> Result, Box> { - self.send_or_disconnect(Request::Variables { - vars - })?; + pub fn get_variables( + &mut self, + vars: VariablesRef, + ) -> Result, Box> { + self.send_or_disconnect(Request::Variables { vars })?; match self.read_response_or_disconnect()? { Response::Variables { vars } => Ok(vars), @@ -353,7 +409,10 @@ impl Auxtools { self.last_error.read().unwrap().clone() } - pub fn set_catch_runtimes(&mut self, should_catch: bool) -> Result<(), Box> { + pub fn set_catch_runtimes( + &mut self, + should_catch: bool, + ) -> Result<(), Box> { self.send_or_disconnect(Request::CatchRuntimes { should_catch }) } } @@ -367,19 +426,19 @@ impl AuxtoolsThread { thread::spawn(move || match listener.accept() { Ok((stream, _)) => { match connection_sender.send(stream.try_clone().unwrap()) { - Ok(_) => {} + Ok(_) => {}, Err(e) => { - eprintln!("Debug client thread failed to pass cloned TcpStream: {}", e); + eprintln!("Debug client thread failed to pass cloned TcpStream: {e}"); return; - } + }, } self.run(stream); - } + }, Err(e) => { - eprintln!("Debug client failed to accept connection: {}", e); - } + eprintln!("Debug client failed to accept connection: {e}"); + }, }) } @@ -392,7 +451,7 @@ impl AuxtoolsThread { Response::Notification { message: _message } => { debug_output!(in self.seq, "[auxtools] {}", _message); - } + }, Response::BreakpointHit { reason } => { let mut description = None; @@ -402,10 +461,10 @@ impl AuxtoolsThread { BreakpointReason::Pause => dap_types::StoppedEvent::REASON_PAUSE, BreakpointReason::Breakpoint => dap_types::StoppedEvent::REASON_BREAKPOINT, BreakpointReason::Runtime(error) => { - *(self.last_error.write().unwrap()) = error.clone(); + self.last_error.write().unwrap().clone_from(&error); description = Some(error); dap_types::StoppedEvent::REASON_EXCEPTION - } + }, }; self.seq.issue_event(dap_types::StoppedEvent { @@ -415,11 +474,11 @@ impl AuxtoolsThread { allThreadsStopped: Some(true), ..Default::default() }); - } + }, x => { self.responses.send(x)?; - } + }, } Ok(false) @@ -438,9 +497,9 @@ impl AuxtoolsThread { Ok(_) => u32::from_le_bytes(len_bytes), Err(e) => { - eprintln!("Debug server thread read error: {}", e); + eprintln!("Debug server thread read error: {e}"); break; - } + }, }; buf.resize(len as usize, 0); @@ -448,9 +507,9 @@ impl AuxtoolsThread { Ok(_) => (), Err(e) => { - eprintln!("Debug server thread read error: {}", e); + eprintln!("Debug server thread read error: {e}"); break; - } + }, }; match self.handle_response(&buf[..]) { @@ -459,12 +518,12 @@ impl AuxtoolsThread { eprintln!("Debug server disconnected"); break; } - } + }, Err(e) => { - eprintln!("Debug server thread failed to handle request: {}", e); + eprintln!("Debug server thread failed to handle request: {e}"); break; - } + }, } } @@ -477,7 +536,9 @@ pub struct UnexpectedResponse(String); impl UnexpectedResponse { fn new(expected: &'static str, received: Response) -> Self { - Self(format!("received unexpected response: expected {}, got {:?}", expected, received)) + Self(format!( + "received unexpected response: expected {expected}, got {received:?}" + )) } } diff --git a/crates/dm-langserver/src/debugger/auxtools_bundle.rs b/crates/dm-langserver/src/debugger/auxtools_bundle.rs index 3eb1e2a6..3790d296 100644 --- a/crates/dm-langserver/src/debugger/auxtools_bundle.rs +++ b/crates/dm-langserver/src/debugger/auxtools_bundle.rs @@ -3,7 +3,7 @@ use std::fs::File; use std::io::{Result, Write}; use std::path::{Path, PathBuf}; -const BYTES: &[u8] = include_bytes!(env!("AUXTOOLS_BUNDLE_DLL")); +const BYTES: &[u8] = include_bytes!(env!("BUNDLE_PATH_debug_server.dll")); fn write(path: &Path) -> Result<()> { File::create(path)?.write_all(BYTES) @@ -13,7 +13,7 @@ pub fn extract() -> Result { let exe = std::env::current_exe()?; let directory = exe.parent().unwrap(); for i in 0..9 { - let dll = directory.join(format!("auxtools_debug_server{}.dll", i)); + let dll = directory.join(format!("auxtools_debug_server{i}.dll")); if let Ok(()) = write(&dll) { return Ok(dll); } diff --git a/crates/dm-langserver/src/debugger/evaluate.rs b/crates/dm-langserver/src/debugger/evaluate.rs index ccd2a084..508a076c 100644 --- a/crates/dm-langserver/src/debugger/evaluate.rs +++ b/crates/dm-langserver/src/debugger/evaluate.rs @@ -1,5 +1,5 @@ -use dap_types::*; use super::*; +use dap_types::*; const EXTOOLS_HELP: &str = " #dis, #disassemble: show disassembly for current stack frame"; @@ -19,37 +19,38 @@ impl Debugger { return Ok(EvaluateResponse::from(EXTOOLS_HELP.trim())); } - guard!(let Some(frame_id) = params.frameId else { - return Err(Box::new(GenericError("Must select a stack frame to evaluate in"))); - }); + let Some(frame_id) = params.frameId else { + return Err(Box::new(GenericError( + "Must select a stack frame to evaluate in", + ))); + }; let (thread, frame_no) = extools.get_thread_by_frame_id(frame_id)?; if input.starts_with('#') { if input == "#dis" || input == "#disassemble" { - guard!(let Some(frame) = thread.call_stack.get(frame_no) else { + let Some(frame) = thread.call_stack.get(frame_no) else { return Err(Box::new(GenericError("Stack frame out of range"))); - }); + }; let bytecode = extools.bytecode(&frame.proc, frame.override_id); return Ok(EvaluateResponse::from(Self::format_disassembly(bytecode))); + } else { + return Err(Box::new(GenericError("Unknown #command"))); } } - } + }, DebugClient::Auxtools(auxtools) => { - let response = auxtools.eval( - params.frameId.map(|x| x as u32), - input, - params.context, - )?; + let response = + auxtools.eval(params.frameId.map(|x| x as u32), input, params.context)?; return Ok(EvaluateResponse { result: response.value, variablesReference: response.variables.map(|x| x.0 as i64).unwrap_or(0), ..Default::default() }); - } + }, } Err(Box::new(GenericError("Not yet implemented"))) diff --git a/crates/dm-langserver/src/debugger/extools.rs b/crates/dm-langserver/src/debugger/extools.rs index 968ad703..6c2fda72 100644 --- a/crates/dm-langserver/src/debugger/extools.rs +++ b/crates/dm-langserver/src/debugger/extools.rs @@ -1,16 +1,14 @@ //! Client for the Extools debugger protocol. -use std::time::Duration; -use std::sync::{mpsc, Arc, Mutex}; -use std::net::{SocketAddr, Ipv4Addr, TcpStream, TcpListener}; -use std::collections::HashMap; -use std::io::{Read, Write}; +use foldhash::{HashMap, HashMapExt}; use std::error::Error; +use std::io::{Read, Write}; +use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::Duration; -use ahash::RandomState; - -use super::SequenceNumber; use super::extools_types::*; +use super::SequenceNumber; pub const DEFAULT_PORT: u16 = 2448; @@ -34,6 +32,7 @@ enum ExtoolsHolderInner { /// Used to avoid a layer of Option. None, Listening { + #[allow(dead_code)] port: u16, conn_rx: mpsc::Receiver, }, @@ -41,7 +40,7 @@ enum ExtoolsHolderInner { cancel_tx: mpsc::Sender<()>, conn_rx: mpsc::Receiver, }, - Active(Extools), + Active(Box), } impl Default for ExtoolsHolder { @@ -59,7 +58,7 @@ impl ExtoolsHolder { let (conn_tx, conn_rx) = mpsc::channel(); std::thread::Builder::new() - .name(format!("extools listening on port {}", port)) + .name(format!("extools listening on port {port}")) .spawn(move || { let stream = match listener.accept() { Ok((stream, _)) => stream, @@ -72,10 +71,10 @@ impl ExtoolsHolder { } })?; - Ok((port, ExtoolsHolder(ExtoolsHolderInner::Listening { + Ok(( port, - conn_rx, - }))) + ExtoolsHolder(ExtoolsHolderInner::Listening { port, conn_rx }), + )) } pub fn attach(seq: Arc, port: u16) -> std::io::Result { @@ -86,10 +85,12 @@ impl ExtoolsHolder { let (cancel_tx, cancel_rx) = mpsc::channel(); std::thread::Builder::new() - .name(format!("extools attaching on port {}", port)) + .name(format!("extools attaching on port {port}")) .spawn(move || { while let Err(mpsc::TryRecvError::Empty) = cancel_rx.try_recv() { - if let Ok(stream) = TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(5)) { + if let Ok(stream) = + TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(5)) + { let (conn, mut thread) = Extools::from_stream(seq, stream); if conn_tx.send(conn).is_ok() { thread.read_loop(); @@ -106,30 +107,35 @@ impl ExtoolsHolder { } pub fn get(&mut self) -> Result<&mut Extools, Box> { - self.as_ref().ok_or_else(|| Box::new(super::GenericError("No extools connection")) as Box) + self.as_ref() + .ok_or_else(|| Box::new(super::GenericError("No extools connection")) as Box) } pub fn as_ref(&mut self) -> Option<&mut Extools> { match &mut self.0 { - ExtoolsHolderInner::Listening { conn_rx, .. } | - ExtoolsHolderInner::Attaching { conn_rx, .. } => { + ExtoolsHolderInner::Listening { conn_rx, .. } + | ExtoolsHolderInner::Attaching { conn_rx, .. } => { if let Ok(conn) = conn_rx.try_recv() { - self.0 = ExtoolsHolderInner::Active(conn); + self.0 = ExtoolsHolderInner::Active(Box::new(conn)); } - } - _ => {} + }, + _ => {}, } match &mut self.0 { ExtoolsHolderInner::Active(conn) => Some(conn), - _ => None + _ => None, } } pub fn disconnect(&mut self) { + // This part of code is not complete, we don't want to use matches! + #[allow(clippy::single_match)] match std::mem::replace(&mut self.0, ExtoolsHolderInner::None) { - ExtoolsHolderInner::Attaching { cancel_tx, .. } => { let _ = cancel_tx.send(()); }, + ExtoolsHolderInner::Attaching { cancel_tx, .. } => { + let _ = cancel_tx.send(()); + }, // TODO: ExtoolsHolderInner::Listening - _ => {} + _ => {}, } } } @@ -139,7 +145,7 @@ pub struct Extools { seq: Arc, sender: ExtoolsSender, threads: Arc>>, - bytecode: HashMap<(String, usize), Vec, RandomState>, + bytecode: HashMap<(String, usize), Vec>, get_type_rx: mpsc::Receiver, bytecode_rx: mpsc::Receiver, get_field_rx: mpsc::Receiver, @@ -171,7 +177,7 @@ impl Extools { seq, sender, threads: Arc::new(Mutex::new(HashMap::new())), - bytecode: HashMap::with_hasher(RandomState::default()), + bytecode: HashMap::new(), bytecode_rx, get_type_rx, get_field_rx, @@ -197,27 +203,43 @@ impl Extools { (extools, thread) } - pub fn get_all_threads(&self) -> std::sync::MutexGuard> { + pub fn get_all_threads(&self) -> std::sync::MutexGuard<'_, HashMap> { self.threads.lock().unwrap() } pub fn get_thread(&self, thread_id: i64) -> Result> { - self.threads.lock().unwrap().get(&thread_id).cloned() - .ok_or_else(|| Box::new(super::GenericError("Getting call stack failed")) as Box) + self.threads + .lock() + .unwrap() + .get(&thread_id) + .cloned() + .ok_or_else(|| { + Box::new(super::GenericError("Getting call stack failed")) as Box + }) } - pub fn get_thread_by_frame_id(&self, frame_id: i64) -> Result<(ThreadInfo, usize), Box> { + pub fn get_thread_by_frame_id( + &self, + frame_id: i64, + ) -> Result<(ThreadInfo, usize), Box> { let frame_id = frame_id as usize; let threads = self.threads.lock().unwrap(); let thread_id = (frame_id % threads.len()) as i64; let frame_no = frame_id / threads.len(); - let thread = threads.get(&thread_id).cloned() - .ok_or_else(|| Box::new(super::GenericError("Getting call stack failed")) as Box)?; + let thread = threads.get(&thread_id).cloned().ok_or_else(|| { + Box::new(super::GenericError("Getting call stack failed")) as Box + })?; Ok((thread, frame_no)) } pub fn bytecode(&mut self, proc_ref: &str, override_id: usize) -> &[DisassembledInstruction] { - let Extools { bytecode, sender, seq: _seq, bytecode_rx, .. } = self; + let Extools { + bytecode, + sender, + seq: _seq, + bytecode_rx, + .. + } = self; bytecode.entry((proc_ref.to_owned(), override_id)).or_insert_with(|| { debug_output!(in _seq, "[extools] Fetching bytecode for {}#{}", proc_ref, override_id); sender.send(ProcDisassemblyRequest(ProcId { @@ -228,7 +250,12 @@ impl Extools { }) } - pub fn offset_to_line(&mut self, proc_ref: &str, override_id: usize, offset: i64) -> Option { + pub fn offset_to_line( + &mut self, + proc_ref: &str, + override_id: usize, + offset: i64, + ) -> Option { let bc = self.bytecode(proc_ref, override_id); let mut comment = ""; for instr in bc.iter() { @@ -261,11 +288,19 @@ impl Extools { } pub fn set_breakpoint(&self, proc: &str, override_id: usize, offset: i64) { - self.sender.send(BreakpointSet(ProcOffset { proc: proc.to_owned(), override_id, offset })); + self.sender.send(BreakpointSet(ProcOffset { + proc: proc.to_owned(), + override_id, + offset, + })); } pub fn unset_breakpoint(&self, proc: &str, override_id: usize, offset: i64) { - self.sender.send(BreakpointUnset(ProcOffset { proc: proc.to_owned(), override_id, offset })); + self.sender.send(BreakpointUnset(ProcOffset { + proc: proc.to_owned(), + override_id, + offset, + })); } pub fn continue_execution(&self) { @@ -292,13 +327,17 @@ impl Extools { self.sender.send(Pause); } + #[allow(dead_code)] pub fn get_reference_type(&self, reference: Ref) -> Result> { // TODO: error handling self.sender.send(GetType(reference)); Ok(self.get_type_rx.recv_timeout(RECV_TIMEOUT)?.0) } - pub fn get_all_fields(&self, reference: Ref) -> Result, Box> { + pub fn get_all_fields( + &self, + reference: Ref, + ) -> Result, Box> { self.sender.send(GetAllFields(reference)); Ok(self.get_field_rx.recv_timeout(RECV_TIMEOUT)?.0) } @@ -337,9 +376,8 @@ impl Drop for Extools { } fn parse_lineno(comment: &str) -> Option { - let prefix = "Line number: "; - if comment.starts_with(prefix) { - comment[prefix.len()..].parse::().ok() + if let Some(rest) = comment.strip_prefix("Line number: ") { + rest.parse::().ok() } else { None } @@ -372,8 +410,8 @@ impl ExtoolsThread { terminator = Some(buffer.len() + pos); } buffer.extend_from_slice(slice); - } - Err(e) => panic!("extools read error: {:?}", e), + }, + Err(e) => panic!("extools read error: {e:?}"), } // chop off as many full messages from the buffer as we can @@ -421,96 +459,140 @@ impl ExtoolsThread { reason: "sleep".to_owned(), threadId: Some(k), preserveFocusHint: Some(true), - .. Default::default() + ..Default::default() }); } } self.seq.issue_event(dap_types::StoppedEvent { threadId: Some(0), - .. base + ..base }); } } -handle_extools! { - on Raw(&mut self, Raw(message)) { +type R = Result<(), Box>; + +macro_rules! handle_response_table { + ($($what:ident;)*) => { + fn handle_response_table(type_: &str) -> Option Result<(), Box>> { + match type_ { + $(<$what as Response>::TYPE => { + Some(|this, content| { + let deserialized: $what = serde_json::from_value(content)?; + this.$what(deserialized) + }) + },)* + _ => None + } + } + } +} + +#[allow(non_snake_case)] +impl ExtoolsThread { + handle_response_table! { + Raw; + BreakpointSet; + BreakpointUnset; + BreakpointHit; + Runtime; + CallStack; + DisassembledProc; + GetTypeResponse; + GetAllFieldsResponse; + ListContents; + GetSource; + BreakOnRuntime; + } + + fn Raw(&mut self, Raw(message): Raw) -> R { output!(in self.seq, "[extools] Message: {}", message); + Ok(()) } - on BreakpointSet(&mut self, BreakpointSet(_bp)) { + fn BreakpointSet(&mut self, BreakpointSet(_bp): BreakpointSet) -> R { debug_output!(in self.seq, "[extools] {}#{}@{} validated", _bp.proc, _bp.override_id, _bp.offset); + Ok(()) } - on BreakpointUnset(&mut self, _) { + fn BreakpointUnset(&mut self, _: BreakpointUnset) -> R { // silent + Ok(()) } - on BreakpointHit(&mut self, hit) { + fn BreakpointHit(&mut self, hit: BreakpointHit) -> R { match hit.reason { BreakpointHitReason::Step => { self.stopped(dap_types::StoppedEvent { reason: dap_types::StoppedEvent::REASON_STEP.to_owned(), - .. Default::default() + ..Default::default() }); - } - BreakpointHitReason::Pause => { - self.stopped(dap_types::StoppedEvent { - reason: dap_types::StoppedEvent::REASON_PAUSE.to_owned(), - description: Some("Paused by request".to_owned()), - .. Default::default() - }) - } + }, + BreakpointHitReason::Pause => self.stopped(dap_types::StoppedEvent { + reason: dap_types::StoppedEvent::REASON_PAUSE.to_owned(), + description: Some("Paused by request".to_owned()), + ..Default::default() + }), _ => { debug_output!(in self.seq, "[extools] {}#{}@{} hit", hit.proc, hit.override_id, hit.offset); self.stopped(dap_types::StoppedEvent { reason: dap_types::StoppedEvent::REASON_BREAKPOINT.to_owned(), - .. Default::default() + ..Default::default() }); - } + }, } + Ok(()) } - on Runtime(&mut self, runtime) { + fn Runtime(&mut self, runtime: Runtime) -> R { output!(in self.seq, "[extools] Runtime in {}: {}", runtime.proc, runtime.message); self.stopped(dap_types::StoppedEvent { reason: dap_types::StoppedEvent::REASON_EXCEPTION.to_owned(), text: Some(runtime.message.clone()), - .. Default::default() + ..Default::default() }); self.queue(&self.runtime_tx, runtime); + Ok(()) } - on CallStack(&mut self, stack) { + fn CallStack(&mut self, stack: CallStack) -> R { let mut map = self.threads.lock().unwrap(); map.clear(); map.entry(0).or_default().call_stack = stack.current; for (i, list) in stack.suspended.into_iter().enumerate() { map.entry((i + 1) as i64).or_default().call_stack = list; } + Ok(()) } - on DisassembledProc(&mut self, disasm) { + fn DisassembledProc(&mut self, disasm: DisassembledProc) -> R { self.queue(&self.bytecode_tx, disasm); + Ok(()) } - on GetTypeResponse(&mut self, response) { + fn GetTypeResponse(&mut self, response: GetTypeResponse) -> R { self.queue(&self.get_type_tx, response); + Ok(()) } - on GetAllFieldsResponse(&mut self, response) { + fn GetAllFieldsResponse(&mut self, response: GetAllFieldsResponse) -> R { self.queue(&self.get_field_tx, response); + Ok(()) } - on ListContents(&mut self, response) { + fn ListContents(&mut self, response: ListContents) -> R { self.queue(&self.get_list_contents_tx, response); + Ok(()) } - on GetSource(&mut self, response) { + fn GetSource(&mut self, response: GetSource) -> R { self.queue(&self.get_source_tx, response); + Ok(()) } - on BreakOnRuntime(&mut self, _) { + fn BreakOnRuntime(&mut self, _: BreakOnRuntime) -> R { // Either it worked or it didn't, nothing we can do about it now. + Ok(()) } } @@ -523,7 +605,10 @@ impl Clone for ExtoolsSender { fn clone(&self) -> ExtoolsSender { ExtoolsSender { seq: self.seq.clone(), - stream: self.stream.try_clone().expect("TcpStream::try_clone failed in ExtoolsSender::clone") + stream: self + .stream + .try_clone() + .expect("TcpStream::try_clone failed in ExtoolsSender::clone"), } } } @@ -534,9 +619,12 @@ impl ExtoolsSender { let mut buffer = serde_json::to_vec(&ProtocolMessage { type_: M::TYPE.to_owned(), content: Some(content), - }).expect("extools encode error"); + }) + .expect("extools encode error"); buffer.push(0); // TODO: needs more synchronization - (&self.stream).write_all(&buffer[..]).expect("extools write error"); + (&self.stream) + .write_all(&buffer[..]) + .expect("extools write error"); } } diff --git a/crates/dm-langserver/src/debugger/extools_bundle.rs b/crates/dm-langserver/src/debugger/extools_bundle.rs index 02396526..713029c7 100644 --- a/crates/dm-langserver/src/debugger/extools_bundle.rs +++ b/crates/dm-langserver/src/debugger/extools_bundle.rs @@ -1,9 +1,9 @@ #![cfg(extools_bundle)] +use std::fs::File; use std::io::{Result, Write}; use std::path::{Path, PathBuf}; -use std::fs::File; -const BYTES: &[u8] = include_bytes!(env!("EXTOOLS_BUNDLE_DLL")); +const BYTES: &[u8] = include_bytes!(env!("BUNDLE_PATH_extools.dll")); fn write(path: &Path) -> Result<()> { File::create(path)?.write_all(BYTES) @@ -13,7 +13,7 @@ pub fn extract() -> Result { let exe = std::env::current_exe()?; let directory = exe.parent().unwrap(); for i in 0..9 { - let dll = directory.join(format!("extools{}.dll", i)); + let dll = directory.join(format!("extools{i}.dll")); if let Ok(()) = write(&dll) { return Ok(dll); } diff --git a/crates/dm-langserver/src/debugger/extools_types.rs b/crates/dm-langserver/src/debugger/extools_types.rs index 131aeeb9..783f2811 100644 --- a/crates/dm-langserver/src/debugger/extools_types.rs +++ b/crates/dm-langserver/src/debugger/extools_types.rs @@ -1,15 +1,13 @@ //! Serde types for the Extools debugger protocol. //! //! * https://github.com/MCHSL/extools/blob/master/byond-extools/src/debug_server/protocol.h -/// -/// > All communication happens over a TCP socket using a JSON-based protocol. -/// > A null byte signifies the end of a message. - -use std::collections::HashMap; +//! +//! > All communication happens over a TCP socket using a JSON-based protocol. +//! > A null byte signifies the end of a message. +#![allow(dead_code)] +use foldhash::HashMap; use serde_json::Value as Json; -use ahash::RandomState; - // ---------------------------------------------------------------------------- // Extools data structures @@ -134,11 +132,14 @@ impl ValueText { let ref_ = Ref(raw); let is_list = raw >> 24 == 0x0F; - (ValueText { - literal: Literal::Ref(ref_), - has_vars: !is_list, - is_list, - }, ref_) + ( + ValueText { + literal: Literal::Ref(ref_), + has_vars: !is_list, + is_list, + }, + ref_, + ) } pub fn to_variables_reference(&self) -> i64 { @@ -154,7 +155,7 @@ impl std::fmt::Display for Ref { match *self { Ref::NULL => fmt.write_str("null"), Ref::WORLD => fmt.write_str("world"), - Ref(v) => write!(fmt, "[0x{:08x}]", v), + Ref(v) => write!(fmt, "[0x{v:08x}]"), } } } @@ -162,17 +163,15 @@ impl std::fmt::Display for Ref { impl std::fmt::Display for Literal { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - Literal::Ref(v) => write!(fmt, "{}", v), - Literal::Number(n) => write!(fmt, "{}", n), - Literal::String(s) => write!(fmt, "{:?}", s), - Literal::Typepath(t) => write!(fmt, "{}", t), - Literal::Resource(f) => write!(fmt, "'{}'", f), - Literal::Proc(p) => { - match p.rfind('/') { - Some(idx) => write!(fmt, "{}/proc/{}", &p[..idx], &p[idx + 1..]), - None => write!(fmt, "{}", p), - } - } + Literal::Ref(v) => write!(fmt, "{v}"), + Literal::Number(n) => write!(fmt, "{n}"), + Literal::String(s) => write!(fmt, "{s:?}"), + Literal::Typepath(t) => write!(fmt, "{t}"), + Literal::Resource(f) => write!(fmt, "'{f}'"), + Literal::Proc(p) => match p.rfind('/') { + Some(idx) => write!(fmt, "{}/proc/{}", &p[..idx], &p[idx + 1..]), + None => write!(fmt, "{p}"), + }, } } } @@ -330,7 +329,7 @@ impl Request for GetAllFields { } #[derive(Deserialize, Debug)] -pub struct GetAllFieldsResponse(pub HashMap); +pub struct GetAllFieldsResponse(pub HashMap); impl Response for GetAllFieldsResponse { const TYPE: &'static str = "get all fields"; diff --git a/crates/dm-langserver/src/debugger/launched.rs b/crates/dm-langserver/src/debugger/launched.rs index 2a4c8374..299aedfe 100644 --- a/crates/dm-langserver/src/debugger/launched.rs +++ b/crates/dm-langserver/src/debugger/launched.rs @@ -2,6 +2,7 @@ #![allow(unsafe_code)] use super::SequenceNumber; +use foldhash::HashMap; use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; @@ -30,24 +31,26 @@ pub struct Launched { pub enum EngineParams { Extools { port: u16, - dll: Option + dll: Option, }, Auxtools { port: u16, - dll: Option - } + dll: Option, + }, } impl Launched { pub fn new( seq: Arc, dreamseeker_exe: &str, + env: Option<&HashMap>, dmb: &str, params: Option, ) -> std::io::Result { let mut command = Command::new(dreamseeker_exe); - #[cfg(unix)] { + #[cfg(unix)] + { if let Some(parent) = std::path::Path::new(dreamseeker_exe).parent() { command.env("LD_LIBRARY_PATH", parent); } @@ -60,6 +63,10 @@ impl Launched { .stdout(Stdio::piped()) .stderr(Stdio::piped()); + if let Some(env) = env { + command.envs(env); + } + match params { Some(EngineParams::Extools { port, dll }) => { command.env("EXTOOLS_MODE", "LAUNCHED"); @@ -67,7 +74,7 @@ impl Launched { if let Some(dll) = dll { command.env("EXTOOLS_DLL", dll); } - } + }, Some(EngineParams::Auxtools { port, dll }) => { command.env("AUXTOOLS_DEBUG_MODE", "LAUNCHED"); @@ -75,7 +82,7 @@ impl Launched { if let Some(dll) = dll { command.env("AUXTOOLS_DEBUG_DLL", dll); } - } + }, None => (), } @@ -134,12 +141,12 @@ impl Launched { true => Ok(()), false => Err(std::io::Error::last_os_error()), } - } + }, State::Exited => Ok(()), _other => { debug_output!(in self.seq, "[launched] kill no-op in state {:?}", _other); Ok(()) - } + }, } } @@ -149,18 +156,24 @@ impl Launched { State::Active => { output!(in self.seq, "[launched] Detaching from child process..."); *state = State::Detached; - } + }, _other => { debug_output!(in self.seq, "[launched] detach no-op in state {:?}", _other); - } + }, } } } -fn pipe_output(seq: Arc, keyword: &'static str, stream: Option) -> std::io::Result<()> { - guard!(let Some(stream2) = stream else { return Ok(()); }); +fn pipe_output( + seq: Arc, + keyword: &'static str, + stream: Option, +) -> std::io::Result<()> { + let Some(stream2) = stream else { + return Ok(()); + }; std::thread::Builder::new() - .name(format!("launched debuggee {} relay", keyword)) + .name(format!("launched debuggee {keyword} relay")) .spawn(move || { use std::io::BufRead; @@ -176,15 +189,15 @@ fn pipe_output(seq: Arc, keyw category: Some(keyword.to_owned()), ..Default::default() }); - } + }, Err(e) => { seq.issue_event(dap_types::OutputEvent { - output: format!("[launched {}] {}", keyword, e), + output: format!("[launched {keyword}] {e}"), category: Some("console".to_owned()), ..Default::default() }); break; - } + }, } } })?; diff --git a/crates/dm-langserver/src/debugger/mod.rs b/crates/dm-langserver/src/debugger/mod.rs index f990a108..5dd5d434 100644 --- a/crates/dm-langserver/src/debugger/mod.rs +++ b/crates/dm-langserver/src/debugger/mod.rs @@ -1,7 +1,8 @@ //! Debug adapter protocol implementation for DreamSeeker. //! //! * https://microsoft.github.io/debug-adapter-protocol/ -#![allow(dead_code)] +// In BYOND references 0xAA_BBBBBB, A is the the type and B is the instance ID. +#![allow(clippy::unusual_byte_groupings)] macro_rules! output { (in $seq:expr, $fmt:expr) => { @@ -36,7 +37,7 @@ mod extools_bundle; mod extools_types; mod launched; -use std::collections::{HashMap, HashSet}; +use foldhash::{HashMap, HashMapExt, HashSet, HashSetExt}; use std::error::Error; use std::sync::{atomic, Arc, Mutex}; @@ -46,16 +47,19 @@ use dreammaker::config::DebugEngine; use auxtools::Auxtools; -use ahash::RandomState; - -use dap_types::*; +use self::auxtools::AuxtoolsScopes; use self::extools::ExtoolsHolder; -use self::launched::{Launched, EngineParams}; +use self::launched::{EngineParams, Launched}; use crate::jrpc_io; +use dap_types::*; + +/// line, path, name, override_id +pub type LineNumber = (i64, String, String, usize); pub fn start_server( engine: DebugEngine, dreamseeker_exe: String, + env: Option>, db: DebugDatabaseBuilder, ) -> std::io::Result<(u16, std::thread::JoinHandle<()>)> { use std::net::*; @@ -64,12 +68,12 @@ pub fn start_server( let port = listener.local_addr()?.port(); let handle = std::thread::Builder::new() - .name(format!("DAP listener on port {}", port)) + .name(format!("DAP listener on port {port}")) .spawn(move || { let (stream, _) = listener.accept().unwrap(); drop(listener); let mut input = std::io::BufReader::new(stream.try_clone().unwrap()); - let mut debugger = Debugger::new(engine, dreamseeker_exe, db, Box::new(stream)); + let mut debugger = Debugger::new(engine, dreamseeker_exe, env, db, Box::new(stream)); jrpc_io::run_with_read(&mut input, |message| debugger.handle_input(message)); })?; @@ -87,20 +91,20 @@ pub fn debugger_main>(mut args: I) { .expect("must specify a value for --dreamseeker-exe"), ); } else { - panic!("unknown argument {:?}", arg); + panic!("unknown argument {arg:?}"); } } let dreamseeker_exe = dreamseeker_exe.expect("must provide argument `--dreamseeker-exe path/to/dreamseeker.exe`"); - eprintln!("dreamseeker: {}", dreamseeker_exe); + eprintln!("dreamseeker: {dreamseeker_exe}"); // This isn't the preferred way to run the DAP server so it's okay for it // to be kind of sloppy. let environment = dm::detect_environment_default() .expect("detect .dme error") .expect("did not detect a .dme"); - let ctx = dm::Context::default(); + let mut ctx = dm::Context::default(); ctx.autodetect_config(&environment); let mut pp = dm::preprocessor::Preprocessor::new(&ctx, environment).unwrap(); let objtree = { @@ -117,7 +121,13 @@ pub fn debugger_main>(mut args: I) { extools_dll: None, debug_server_dll: None, }; - let mut debugger = Debugger::new(ctx.config().debugger.engine, dreamseeker_exe, db, Box::new(std::io::stdout())); + let mut debugger = Debugger::new( + ctx.config().debugger.engine, + dreamseeker_exe, + None, + db, + Box::new(std::io::stdout()), + ); jrpc_io::run_until_stdin_eof(|message| debugger.handle_input(message)); } @@ -138,8 +148,7 @@ impl DebugDatabaseBuilder { extools_dll: _, debug_server_dll: _, } = self; - let mut line_numbers: HashMap, RandomState> = - HashMap::with_hasher(RandomState::default()); + let mut line_numbers: HashMap> = HashMap::new(); objtree.root().recurse(&mut |ty| { for (name, proc) in ty.procs.iter() { @@ -179,7 +188,7 @@ pub struct DebugDatabase { root_dir: std::path::PathBuf, files: dm::FileList, objtree: Arc, - line_numbers: HashMap, RandomState>, + line_numbers: HashMap>, } fn get_proc<'o>( @@ -192,8 +201,8 @@ fn get_proc<'o>( match bits.last() { Some(&"proc") | Some(&"verb") => { bits.pop(); - } - _ => {} + }, + _ => {}, } let typename = bits.join("/"); @@ -241,6 +250,7 @@ enum DebugClient { struct Debugger { engine: DebugEngine, dreamseeker_exe: String, + env: Option>, extools_dll: Option, debug_server_dll: Option, db: DebugDatabase, @@ -255,10 +265,17 @@ struct Debugger { } impl Debugger { - fn new(engine: DebugEngine, dreamseeker_exe: String, mut db: DebugDatabaseBuilder, stream: OutStream) -> Self { + fn new( + engine: DebugEngine, + dreamseeker_exe: String, + env: Option>, + mut db: DebugDatabaseBuilder, + stream: OutStream, + ) -> Self { Debugger { engine, dreamseeker_exe, + env, extools_dll: db.extools_dll.take(), debug_server_dll: db.debug_server_dll.take(), db: db.build(), @@ -290,7 +307,7 @@ impl Debugger { let handled = match Self::handle_request_table(&request.command) { Some(handler) => { handler(self, request.arguments.unwrap_or(serde_json::Value::Null)) - } + }, None => Err(format!("Request NYI: {}", request.command).into()), }; @@ -310,14 +327,14 @@ impl Debugger { } debug_output!(in self.seq, " - {}", message); None - } + }, }, command, }; self.seq .send_raw(&serde_json::to_string(&response).expect("response encode error")) - } - other => return Err(format!("unknown `type` field {:?}", other).into()), + }, + other => return Err(format!("unknown `type` field {other:?}").into()), } Ok(()) } @@ -347,7 +364,7 @@ impl Debugger { match &mut self.client { DebugClient::Extools(extools) => { let keys: Vec<_> = { - guard!(let Ok(extools) = extools.get() else { return }); + let Ok(extools) = extools.get() else { return }; extools .get_all_threads() .keys() @@ -361,9 +378,10 @@ impl Debugger { threadId: k, }); } - } + }, - DebugClient::Auxtools(auxtools) => for stack in auxtools.get_stacks().unwrap_or(vec![]) { + DebugClient::Auxtools(auxtools) => { + for stack in auxtools.get_stacks().unwrap_or_default() { if stack.id == 0 { continue; } @@ -371,15 +389,61 @@ impl Debugger { reason: dap_types::ThreadEvent::REASON_EXITED.to_owned(), threadId: stack.id as i64, }); - }, + } + }, } } } const EXCEPTION_FILTER_RUNTIMES: &str = "runtimes"; -handle_request! { - on Initialize(&mut self, params) { +type P = ::Params; +type R = Result<::Result, Box>; + +macro_rules! handle_request_table { + ($($what:ident;)*) => { + fn handle_request_table(command: &str) -> Option Result>> { + match command { + $($what::COMMAND => { + Some(|this, arguments| { + let params: <$what as Request>::Params = serde_json::from_value(arguments)?; + let result: <$what as Request>::Result = this.$what(params)?; + Ok(serde_json::to_value(result).expect("encode problem")) + }) + },)* + _ => None + } + } + } +} + +#[allow(non_snake_case)] +impl Debugger { + handle_request_table! { + Initialize; + LaunchVsc; + AttachVsc; + Disconnect; + ConfigurationDone; + Threads; + SetBreakpoints; + SetFunctionBreakpoints; + StackTrace; + Scopes; + Variables; + Continue; + StepIn; + Next; + StepOut; + Pause; + SetExceptionBreakpoints; + ExceptionInfo; + Evaluate; + Source; + Disassemble; + } + + fn Initialize(&mut self, params: P) -> R { // Initialize client caps from request self.client_caps = ClientCaps::parse(¶ms); @@ -396,25 +460,23 @@ handle_request! { // ... clientID, clientName, adapterID, locale, pathFormat // Tell the client our caps - Some(Capabilities { + Ok(Some(Capabilities { supportTerminateDebuggee: Some(true), supportsExceptionInfoRequest: Some(true), supportsConfigurationDoneRequest: Some(true), supportsFunctionBreakpoints: Some(true), supportsConditionalBreakpoints: Some(true), supportsDisassembleRequest: Some(true), - exceptionBreakpointFilters: Some(vec![ - ExceptionBreakpointsFilter { - filter: EXCEPTION_FILTER_RUNTIMES.to_owned(), - label: "Runtime errors".to_owned(), - default: Some(true), - } - ]), - .. Default::default() - }) + exceptionBreakpointFilters: Some(vec![ExceptionBreakpointsFilter { + filter: EXCEPTION_FILTER_RUNTIMES.to_owned(), + label: "Runtime errors".to_owned(), + default: Some(true), + }]), + ..Default::default() + })) } - on LaunchVsc(&mut self, params) { + fn LaunchVsc(&mut self, params: P) -> R { // Determine port number to pass if debugging is enabled. let debug = !params.base.noDebug.unwrap_or(false); @@ -428,14 +490,13 @@ handle_request! { #[allow(unused_mut)] let mut extools_dll = None; - #[cfg(debug_assertions)] { - if let Some(dll) = self.extools_dll.as_ref() { - debug_output!(in self.seq, "[main] configured override: {:?}", dll); - extools_dll = Some(dll.into()); - } + if let Some(dll) = self.extools_dll.as_ref() { + debug_output!(in self.seq, "[main] configured override: {:?}", dll); + extools_dll = Some(dll.into()); } - #[cfg(extools_bundle)] { + #[cfg(extools_bundle)] + { if extools_dll.is_none() { extools_dll = Some(self::extools_bundle::extract()?); } @@ -445,7 +506,7 @@ handle_request! { port, dll: extools_dll, } - } + }, DebugEngine::Auxtools => { let (port, auxtools) = Auxtools::listen(self.seq.clone())?; @@ -454,14 +515,13 @@ handle_request! { #[allow(unused_mut)] let mut debug_server_dll = None; - #[cfg(debug_assertions)] { - if let Some(dll) = self.debug_server_dll.as_ref() { - debug_output!(in self.seq, "[main] configured override: {:?}", dll); - debug_server_dll = Some(dll.into()); - } + if let Some(dll) = self.debug_server_dll.as_ref() { + debug_output!(in self.seq, "[main] configured override: {:?}", dll); + debug_server_dll = Some(dll.into()); } - #[cfg(auxtools_bundle)] { + #[cfg(auxtools_bundle)] + { if debug_server_dll.is_none() { debug_server_dll = Some(self::auxtools_bundle::extract()?); } @@ -471,40 +531,49 @@ handle_request! { port, dll: debug_server_dll, } - } + }, }) } else { None }; // Launch the subprocess. - self.launched = Some(Launched::new(self.seq.clone(), &self.dreamseeker_exe, ¶ms.dmb, engine_params)?); + self.launched = Some(Launched::new( + self.seq.clone(), + &self.dreamseeker_exe, + self.env.as_ref(), + ¶ms.dmb, + engine_params, + )?); + Ok(()) } - on AttachVsc(&mut self, params) { + fn AttachVsc(&mut self, params: P) -> R { self.client = match self.engine { - DebugEngine::Extools => { - DebugClient::Extools(ExtoolsHolder::attach(self.seq.clone(), params.port.unwrap_or(extools::DEFAULT_PORT))?) - } + DebugEngine::Extools => DebugClient::Extools(ExtoolsHolder::attach( + self.seq.clone(), + params.port.unwrap_or(extools::DEFAULT_PORT), + )?), DebugEngine::Auxtools => { DebugClient::Auxtools(Auxtools::connect(self.seq.clone(), params.port)?) - } + }, }; + Ok(()) } - on Disconnect(&mut self, params) { + fn Disconnect(&mut self, params: P) -> R { let default_terminate = self.launched.is_some(); let terminate = params.terminateDebuggee.unwrap_or(default_terminate); match &mut self.client { DebugClient::Extools(extools) => { extools.disconnect(); - } + }, DebugClient::Auxtools(auxtools) => { auxtools.disconnect(); - } + }, } if let Some(launched) = self.launched.take() { @@ -514,9 +583,10 @@ handle_request! { launched.detach(); } } + Ok(()) } - on ConfigurationDone(&mut self, ()) { + fn ConfigurationDone(&mut self, (): P) -> R { match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; @@ -525,19 +595,19 @@ handle_request! { self.stddef_dm_info = Some(StddefDmInfo::new(text)); extools.configuration_done(); - } + }, DebugClient::Auxtools(auxtools) => { - self.stddef_dm_info = auxtools.get_stddef()?.map(|x| StddefDmInfo::new(x)); + self.stddef_dm_info = auxtools.get_stddef()?.map(StddefDmInfo::new); auxtools.configured()?; - } + }, } + Ok(()) } - on Threads(&mut self, ()) { + fn Threads(&mut self, (): P) -> R { match &mut self.client { DebugClient::Extools(extools) => { - let mut threads = Vec::new(); let extools = extools.get()?; @@ -555,46 +625,44 @@ handle_request! { }); } - ThreadsResponse { - threads, - } + Ok(ThreadsResponse { threads }) }, DebugClient::Auxtools(auxtools) => { - let mut threads : Vec = auxtools.get_stacks()?.into_iter().map(|x| { - Thread { + let mut threads: Vec = auxtools + .get_stacks()? + .into_iter() + .map(|x| Thread { id: x.id as i64, name: x.name, - } - }).collect(); + }) + .collect(); // If we tell DAP that there are no threads, Pause requests never get passed through! if threads.is_empty() { - threads.push( - Thread { - id: 0, - name: "Main".to_owned(), - } - ); + threads.push(Thread { + id: 0, + name: "Main".to_owned(), + }); } - ThreadsResponse { - threads, - } - } + Ok(ThreadsResponse { threads }) + }, } } - on SetBreakpoints(&mut self, params) { - guard!(let Some(file_path) = params.source.path else { + fn SetBreakpoints(&mut self, params: P) -> R { + let Some(file_path) = params.source.path else { return Err(Box::new(GenericError("missing .source.path"))); - }); - guard!(let Some(file_id) = self.db.file_id(&file_path) else { + }; + let Some(file_id) = self.db.file_id(&file_path) else { return Err(Box::new(GenericError("file is not part of environment"))); - }); + }; if params.sourceModified.unwrap_or(false) { - return Err(Box::new(GenericError("cannot update breakpoints in modified source"))); + return Err(Box::new(GenericError( + "cannot update breakpoints in modified source", + ))); } let inputs = params.breakpoints.unwrap_or_default(); @@ -605,22 +673,24 @@ handle_request! { DebugClient::Extools(extools) => { let mut breakpoints = Vec::new(); - guard!(let Some(extools) = extools.as_ref() else { + let Some(extools) = extools.as_ref() else { for sbp in inputs { breakpoints.push(Breakpoint { message: Some("Debugging hooks not available".to_owned()), line: Some(sbp.line), verified: false, - .. Default::default() + ..Default::default() }); } return Ok(SetBreakpointsResponse { breakpoints }); - }); + }; for sbp in inputs { - if let Some((typepath, name, override_id)) = self.db.location_to_proc_ref(file_id, sbp.line) { + if let Some((typepath, name, override_id)) = + self.db.location_to_proc_ref(file_id, sbp.line) + { // TODO: better discipline around format!("{}/{}") and so on - let proc = format!("{}/{}", typepath, name); + let proc = format!("{typepath}/{name}"); if let Some(offset) = extools.line_to_offset(&proc, override_id, sbp.line) { let tup = (proc, override_id, offset); if saved.insert(tup.clone()) { @@ -631,7 +701,7 @@ handle_request! { line: Some(sbp.line), verified: true, column: Some(0), - .. Default::default() + ..Default::default() }); } else { debug_output!(in self.seq, @@ -643,7 +713,7 @@ handle_request! { message: Some("Unable to determine offset in proc".to_owned()), line: Some(sbp.line), verified: false, - .. Default::default() + ..Default::default() }); } } else { @@ -651,13 +721,13 @@ handle_request! { message: Some("Unable to determine proc ref".to_owned()), line: Some(sbp.line), verified: false, - .. Default::default() + ..Default::default() }); } } saved.retain(|k| { - if !keep.contains(&k) { + if !keep.contains(k) { extools.unset_breakpoint(&k.0, k.1, k.2); false } else { @@ -665,44 +735,51 @@ handle_request! { } }); - SetBreakpointsResponse { breakpoints } - } + Ok(SetBreakpointsResponse { breakpoints }) + }, DebugClient::Auxtools(auxtools) => { let mut breakpoints = vec![]; for sbp in inputs { - if let Some((typepath, name, override_id)) = self.db.location_to_proc_ref(file_id, sbp.line) { + if let Some((typepath, name, override_id)) = + self.db.location_to_proc_ref(file_id, sbp.line) + { // TODO: better discipline around format!("{}/{}") and so on - let proc = format!("{}/{}", typepath, name); + let proc = format!("{typepath}/{name}"); - if let Some(offset) = auxtools.get_offset(proc.as_str(), override_id as u32, sbp.line as u32)? { + if let Some(offset) = auxtools.get_offset( + proc.as_str(), + override_id as u32, + sbp.line as u32, + )? { saved.insert((proc.clone(), override_id, offset as i64)); keep.insert((proc.clone(), override_id, offset as i64)); - let result = auxtools.set_breakpoint(auxtools_types::InstructionRef { - proc: auxtools_types::ProcRef { - path: proc, - override_id: override_id as u32 + let result = auxtools.set_breakpoint( + auxtools_types::InstructionRef { + proc: auxtools_types::ProcRef { + path: proc, + override_id: override_id as u32, + }, + offset, }, - offset - }, sbp.condition)?; + sbp.condition, + )?; breakpoints.push(match result { auxtools_types::BreakpointSetResult::Success { line } => { Breakpoint { verified: true, line: line.map(|x| x as i64), - .. Default::default() + ..Default::default() } }, - auxtools_types::BreakpointSetResult::Failed => { - Breakpoint { - verified: false, - .. Default::default() - } - } + auxtools_types::BreakpointSetResult::Failed => Breakpoint { + verified: false, + ..Default::default() + }, }); } else { // debug_output!(in self.seq, @@ -714,7 +791,7 @@ handle_request! { message: Some("Unable to determine offset in proc".to_owned()), line: Some(sbp.line), verified: false, - .. Default::default() + ..Default::default() }); } } else { @@ -722,17 +799,17 @@ handle_request! { message: Some("Unable to determine proc ref".to_owned()), line: Some(sbp.line), verified: false, - .. Default::default() + ..Default::default() }); } } saved.retain(|k| { - if !keep.contains(&k) { + if !keep.contains(k) { let _ = auxtools.unset_breakpoint(&auxtools_types::InstructionRef { proc: auxtools_types::ProcRef { path: k.0.clone(), - override_id: k.1 as u32 + override_id: k.1 as u32, }, offset: k.2 as u32, }); @@ -742,12 +819,15 @@ handle_request! { } }); - SetBreakpointsResponse { breakpoints } - } + Ok(SetBreakpointsResponse { breakpoints }) + }, } } - on SetFunctionBreakpoints(&mut self, params) { + fn SetFunctionBreakpoints( + &mut self, + params: P, + ) -> R { let file_id = FileId::default(); let inputs = params.breakpoints; @@ -757,16 +837,16 @@ handle_request! { match &mut self.client { DebugClient::Extools(extools) => { - guard!(let Some(extools) = extools.as_ref() else { + let Some(extools) = extools.as_ref() else { for _ in inputs { breakpoints.push(Breakpoint { message: Some("Debugging hooks not available".to_owned()), verified: false, - .. Default::default() + ..Default::default() }); } return Ok(SetFunctionBreakpointsResponse { breakpoints }); - }); + }; for sbp in inputs { // parse function reference @@ -774,7 +854,7 @@ handle_request! { let mut override_id = 0; if let Some(idx) = sbp.name.find('#') { proc = &sbp.name[..idx]; - override_id = sbp.name[idx+1..].parse()?; + override_id = sbp.name[idx + 1..].parse()?; } if let Some(proc_ref) = self.db.get_proc(proc, override_id) { @@ -788,19 +868,19 @@ handle_request! { line: Some(proc_ref.location.line as i64), verified: true, column: Some(0), - .. Default::default() + ..Default::default() }); } else { breakpoints.push(Breakpoint { - message: Some(format!("Unknown proc {}#{}", proc, override_id)), + message: Some(format!("Unknown proc {proc}#{override_id}")), verified: false, - .. Default::default() + ..Default::default() }); } } saved.retain(|k| { - if !keep.contains(&k) { + if !keep.contains(k) { extools.unset_breakpoint(&k.0, k.1, k.2); false } else { @@ -808,9 +888,8 @@ handle_request! { } }); - SetFunctionBreakpointsResponse { breakpoints } - } - + Ok(SetFunctionBreakpointsResponse { breakpoints }) + }, DebugClient::Auxtools(auxtools) => { let mut breakpoints = vec![]; @@ -821,7 +900,7 @@ handle_request! { let mut override_id = 0; if let Some(idx) = sbp.name.find('#') { proc = &sbp.name[..idx]; - override_id = sbp.name[idx+1..].parse()?; + override_id = sbp.name[idx + 1..].parse()?; } let offset = 0; @@ -830,38 +909,37 @@ handle_request! { saved.insert(tup.clone()); keep.insert(tup.clone()); - let result = auxtools.set_breakpoint(auxtools_types::InstructionRef { - proc: auxtools_types::ProcRef { - path: tup.0, - override_id: override_id as u32 + let result = auxtools.set_breakpoint( + auxtools_types::InstructionRef { + proc: auxtools_types::ProcRef { + path: tup.0, + override_id: override_id as u32, + }, + offset: offset as u32, }, - offset: offset as u32, - }, sbp.condition)?; + sbp.condition, + )?; breakpoints.push(match result { - auxtools_types::BreakpointSetResult::Success { line } => { - Breakpoint { - verified: true, - line: line.map(|x| x as i64), - .. Default::default() - } + auxtools_types::BreakpointSetResult::Success { line } => Breakpoint { + verified: true, + line: line.map(|x| x as i64), + ..Default::default() }, - auxtools_types::BreakpointSetResult::Failed => { - Breakpoint { - verified: false, - .. Default::default() - } - } + auxtools_types::BreakpointSetResult::Failed => Breakpoint { + verified: false, + ..Default::default() + }, }); } saved.retain(|k| { - if !keep.contains(&k) { + if !keep.contains(k) { let _ = auxtools.unset_breakpoint(&auxtools_types::InstructionRef { proc: auxtools_types::ProcRef { path: k.0.clone(), - override_id: k.1 as u32 + override_id: k.1 as u32, }, offset: k.2 as u32, }); @@ -871,12 +949,12 @@ handle_request! { } }); - SetFunctionBreakpointsResponse { breakpoints } - } + Ok(SetFunctionBreakpointsResponse { breakpoints }) + }, } } - on StackTrace(&mut self, params) { + fn StackTrace(&mut self, params: P) -> R { match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; @@ -888,8 +966,11 @@ handle_request! { let mut dap_frame = StackFrame { name: ex_frame.proc.clone(), id: (i * extools.get_all_threads().len()) as i64 + params.threadId, - instructionPointerReference: Some(format!("{}@{}#{}", ex_frame.proc, ex_frame.override_id, ex_frame.offset)), - .. Default::default() + instructionPointerReference: Some(format!( + "{}@{}#{}", + ex_frame.proc, ex_frame.override_id, ex_frame.offset + )), + ..Default::default() }; if i == 0 { @@ -902,11 +983,15 @@ handle_request! { if proc.location.is_builtins() { // `stddef.dm` proc. if let Some(stddef_dm_info) = self.stddef_dm_info.as_ref() { - if let Some(proc) = get_proc(&stddef_dm_info.objtree, &ex_frame.proc, ex_frame.override_id) { + if let Some(proc) = get_proc( + &stddef_dm_info.objtree, + &ex_frame.proc, + ex_frame.override_id, + ) { dap_frame.source = Some(Source { name: Some("stddef.dm".to_owned()), sourceReference: Some(STDDEF_SOURCE_REFERENCE), - .. Default::default() + ..Default::default() }); dap_frame.line = i64::from(proc.location.line); //dap_frame.column = i64::from(proc.location.column); @@ -917,36 +1002,45 @@ handle_request! { let path = self.db.files.get_path(proc.location.file); dap_frame.source = Some(Source { - name: Some(path.file_name() - .unwrap_or_default() - .to_string_lossy() - .into_owned()), - path: Some(self.db.root_dir.join(path).to_string_lossy().into_owned()), - .. Default::default() + name: Some( + path.file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(), + ), + path: Some( + self.db.root_dir.join(&*path).to_string_lossy().into_owned(), + ), + ..Default::default() }); dap_frame.line = i64::from(proc.location.line); //dap_frame.column = i64::from(proc.location.column); } } - if let Some(line) = extools.offset_to_line(&ex_frame.proc, ex_frame.override_id, ex_frame.offset) { + if let Some(line) = extools.offset_to_line( + &ex_frame.proc, + ex_frame.override_id, + ex_frame.offset, + ) { dap_frame.line = line; } frames.push(dap_frame); } - StackTraceResponse { + Ok(StackTraceResponse { totalFrames: Some(len as i64), stackFrames: frames, - } - } + }) + }, DebugClient::Auxtools(auxtools) => { let (aux_frames, aux_frames_total) = auxtools.get_stack_frames( params.threadId as u32, params.startFrame.map(|x| x as u32), - params.levels.map(|x| x as u32))?; + params.levels.map(|x| x as u32), + )?; let mut frames = Vec::with_capacity(aux_frames.len()); for (i, aux_frame) in aux_frames.iter().enumerate() { @@ -954,8 +1048,11 @@ handle_request! { let mut dap_frame = StackFrame { name: aux_proc.path.to_owned(), id: aux_frame.id as i64, - instructionPointerReference: Some(format!("{}@{}#{}", aux_proc.path, aux_proc.override_id, aux_frame.instruction.offset)), - .. Default::default() + instructionPointerReference: Some(format!( + "{}@{}#{}", + aux_proc.path, aux_proc.override_id, aux_frame.instruction.offset + )), + ..Default::default() }; if i == 0 { @@ -964,15 +1061,22 @@ handle_request! { dap_frame.column = 1; } - if let Some(proc) = self.db.get_proc(&aux_proc.path, aux_proc.override_id as usize) { + if let Some(proc) = self + .db + .get_proc(&aux_proc.path, aux_proc.override_id as usize) + { if proc.location.is_builtins() { // `stddef.dm` proc. if let Some(stddef_dm_info) = self.stddef_dm_info.as_ref() { - if let Some(proc) = get_proc(&stddef_dm_info.objtree, &aux_proc.path, aux_proc.override_id as usize) { + if let Some(proc) = get_proc( + &stddef_dm_info.objtree, + &aux_proc.path, + aux_proc.override_id as usize, + ) { dap_frame.source = Some(Source { name: Some("stddef.dm".to_owned()), sourceReference: Some(STDDEF_SOURCE_REFERENCE), - .. Default::default() + ..Default::default() }); dap_frame.line = i64::from(proc.location.line); //dap_frame.column = i64::from(proc.location.column); @@ -983,12 +1087,16 @@ handle_request! { let path = self.db.files.get_path(proc.location.file); dap_frame.source = Some(Source { - name: Some(path.file_name() - .unwrap_or_default() - .to_string_lossy() - .into_owned()), - path: Some(self.db.root_dir.join(path).to_string_lossy().into_owned()), - .. Default::default() + name: Some( + path.file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(), + ), + path: Some( + self.db.root_dir.join(&*path).to_string_lossy().into_owned(), + ), + ..Default::default() }); dap_frame.line = i64::from(proc.location.line); //dap_frame.column = i64::from(proc.location.column); @@ -1002,15 +1110,15 @@ handle_request! { frames.push(dap_frame); } - StackTraceResponse { + Ok(StackTraceResponse { totalFrames: Some(aux_frames_total as i64), stackFrames: frames, - } - } + }) + }, } } - on Scopes(&mut self, ScopesArguments { frameId }) { + fn Scopes(&mut self, ScopesArguments { frameId }: P) -> R { match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; @@ -1020,44 +1128,52 @@ handle_request! { let thread_id = (frame_id % threads.len()) as i64; let frame_no = frame_id / threads.len(); - guard!(let Some(frame) = threads[&thread_id].call_stack.get(frame_no) else { - return Err(Box::new(GenericError2(format!("Stack frame out of range: {} (thread {}, depth {})", frameId, thread_id, frame_no)))); - }); + let Some(frame) = threads[&thread_id].call_stack.get(frame_no) else { + return Err(Box::new(GenericError2(format!("Stack frame out of range: {frameId} (thread {thread_id}, depth {frame_no})")))); + }; - ScopesResponse { + Ok(ScopesResponse { scopes: vec![ Scope { name: "Locals".to_owned(), presentationHint: Some("locals".to_owned()), variablesReference: frameId * 2 + 2, indexedVariables: Some(frame.locals.len() as i64), - .. Default::default() + ..Default::default() }, Scope { name: "Arguments".to_owned(), presentationHint: Some("arguments".to_owned()), variablesReference: frameId * 2 + 1, namedVariables: Some(2 + frame.args.len() as i64), - .. Default::default() + ..Default::default() }, Scope { name: "Globals".to_owned(), variablesReference: 0x0e_000001, - .. Default::default() + ..Default::default() }, - ] - } - } + ], + }) + }, DebugClient::Auxtools(auxtools) => { - let (arguments, locals, globals) = auxtools.get_scopes(frameId as u32)?; - let mut scopes = Vec::with_capacity(locals.is_some() as usize + arguments.is_some() as usize + globals.is_some() as usize); + let AuxtoolsScopes { + arguments, + locals, + globals, + } = auxtools.get_scopes(frameId as u32)?; + let mut scopes = Vec::with_capacity( + locals.is_some() as usize + + arguments.is_some() as usize + + globals.is_some() as usize, + ); if let Some(locals) = locals { scopes.push(Scope { name: "Locals".to_owned(), variablesReference: locals.0 as i64, - .. Default::default() + ..Default::default() }); } @@ -1065,7 +1181,7 @@ handle_request! { scopes.push(Scope { name: "Arguments".to_owned(), variablesReference: arguments.0 as i64, - .. Default::default() + ..Default::default() }); } @@ -1073,24 +1189,24 @@ handle_request! { scopes.push(Scope { name: "Globals".to_owned(), variablesReference: globals.0 as i64, - .. Default::default() + ..Default::default() }); } - ScopesResponse { - scopes - } - } + Ok(ScopesResponse { scopes }) + }, } } - on Variables(&mut self, params) { + fn Variables(&mut self, params: P) -> R { match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; if params.variablesReference >= 0x01_000000 { - let (var, ref_) = extools_types::ValueText::from_variables_reference(params.variablesReference); + let (var, ref_) = extools_types::ValueText::from_variables_reference( + params.variablesReference, + ); let mut variables = Vec::new(); if var.is_list { @@ -1102,26 +1218,26 @@ handle_request! { name: format!("[{}]", 1 + i), value: entry.to_string(), variablesReference: entry.to_variables_reference(), - .. Default::default() + ..Default::default() }); } - } + }, extools_types::ListContents::Associative(entries) => { for (i, (key, val)) in entries.iter().enumerate() { variables.push(Variable { name: format!("keys[{}]", 1 + i), value: key.to_string(), variablesReference: key.to_variables_reference(), - .. Default::default() + ..Default::default() }); variables.push(Variable { name: format!("vals[{}]", 1 + i), value: val.to_string(), variablesReference: val.to_variables_reference(), - .. Default::default() + ..Default::default() }); } - } + }, } } else if var.has_vars { // Datum reference @@ -1133,7 +1249,7 @@ handle_request! { name: name.to_owned(), value: vt.to_string(), variablesReference: vt.to_variables_reference(), - .. Default::default() + ..Default::default() }) } } @@ -1146,28 +1262,28 @@ handle_request! { let mod2 = params.variablesReference % 2; let (thread, frame_no) = extools.get_thread_by_frame_id(frame_id)?; - guard!(let Some(frame) = thread.call_stack.get(frame_no) else { + let Some(frame) = thread.call_stack.get(frame_no) else { return Err(Box::new(GenericError("Stack frame out of range"))); - }); + }; if mod2 == 1 { // arguments let mut variables = Vec::with_capacity(2 + frame.args.len()); - let mut seen = std::collections::HashMap::with_hasher(RandomState::default()); + let mut seen = HashMap::new(); seen.insert("src", 0); variables.push(Variable { name: "src".to_owned(), value: frame.src.to_string(), variablesReference: frame.src.to_variables_reference(), - .. Default::default() + ..Default::default() }); seen.insert("usr", 0); variables.push(Variable { name: "usr".to_owned(), value: frame.usr.to_string(), variablesReference: frame.usr.to_variables_reference(), - .. Default::default() + ..Default::default() }); variables.extend(frame.args.iter().enumerate().map(|(i, vt)| Variable { @@ -1175,16 +1291,16 @@ handle_request! { Some(param) => { match seen.entry(param).and_modify(|e| *e += 1).or_default() { 0 => param.clone(), - n => format!("{} #{}", param, n), + n => format!("{param} #{n}"), } - } + }, None => format!("args[{}]", i + 1), }, value: vt.to_string(), variablesReference: vt.to_variables_reference(), - .. Default::default() + ..Default::default() })); - VariablesResponse { variables } + Ok(VariablesResponse { variables }) } else if mod2 == 0 { // locals let mut variables = Vec::with_capacity(1 + frame.locals.len()); @@ -1193,42 +1309,48 @@ handle_request! { name: ".".to_owned(), value: frame.dot.to_string(), variablesReference: frame.dot.to_variables_reference(), - .. Default::default() + ..Default::default() }); // If VSC receives two Variables with the same name, it only // displays the first one. Avert this by adding suffixes. - let mut seen = std::collections::HashMap::with_hasher(RandomState::default()); + let mut seen = HashMap::new(); variables.extend(frame.locals.iter().enumerate().map(|(i, vt)| Variable { name: match frame.local_names.get(i) { Some(local) => { match seen.entry(local).and_modify(|e| *e += 1).or_default() { 0 => local.clone(), - n => format!("{} #{}", local, n), + n => format!("{local} #{n}"), } - } + }, None => i.to_string(), }, value: vt.to_string(), variablesReference: vt.to_variables_reference(), - .. Default::default() + ..Default::default() })); - VariablesResponse { variables } + Ok(VariablesResponse { variables }) } else { - return Err(Box::new(GenericError("Bad variables reference"))); + Err(Box::new(GenericError("Bad variables reference"))) } - } + }, DebugClient::Auxtools(auxtools) => { - let aux_variables = auxtools.get_variables(auxtools_types::VariablesRef(params.variablesReference as i32))?; + let aux_variables = auxtools.get_variables(auxtools_types::VariablesRef( + params.variablesReference as i32, + ))?; let mut variables = vec![]; // If VSC receives two Variables with the same name, it only // displays the first one. Avert this by adding suffixes. - let mut seen = std::collections::HashMap::with_hasher(RandomState::default()); + let mut seen = HashMap::new(); for aux_var in aux_variables { - let name = match seen.entry(aux_var.name.clone()).and_modify(|e| *e += 1).or_default() { + let name = match seen + .entry(aux_var.name.clone()) + .and_modify(|e| *e += 1) + .or_default() + { 0 => aux_var.name, n => format!("{} #{}", aux_var.name, n), }; @@ -1237,137 +1359,151 @@ handle_request! { name, value: aux_var.value, variablesReference: aux_var.variables.map(|x| x.0 as i64).unwrap_or(0), - .. Default::default() + ..Default::default() }); } - VariablesResponse { - variables - } - } + Ok(VariablesResponse { variables }) + }, } } - on Continue(&mut self, _params) { + fn Continue(&mut self, _params: P) -> R { self.notify_continue(); match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; extools.continue_execution(); - } + }, DebugClient::Auxtools(auxtools) => { auxtools.continue_execution()?; - } + }, } - ContinueResponse { + Ok(ContinueResponse { allThreadsContinued: Some(true), - } + }) } - on StepIn(&mut self, params) { + fn StepIn(&mut self, params: P) -> R { self.notify_continue(); match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; extools.step_in(params.threadId); - } + }, DebugClient::Auxtools(auxtools) => { auxtools.step_into(params.threadId as u32)?; - } + }, } + Ok(()) } - on Next(&mut self, params) { + fn Next(&mut self, params: P) -> R { self.notify_continue(); match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; extools.step_over(params.threadId); - } + }, DebugClient::Auxtools(auxtools) => { auxtools.next(params.threadId as u32)?; - } + }, } + Ok(()) } - on StepOut(&mut self, params) { + fn StepOut(&mut self, params: P) -> R { self.notify_continue(); match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; extools.step_out(params.threadId); - } + }, DebugClient::Auxtools(auxtools) => { auxtools.step_out(params.threadId as u32)?; - } + }, } + Ok(()) } - on Pause(&mut self, _params) { + fn Pause(&mut self, _params: P) -> R { match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; extools.pause(); - } + }, DebugClient::Auxtools(auxtools) => { auxtools.pause()?; - } + }, } + Ok(()) } - on SetExceptionBreakpoints(&mut self, params) { + fn SetExceptionBreakpoints( + &mut self, + params: P, + ) -> R { match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; - extools.set_break_on_runtime(params.filters.iter().any(|x| x == EXCEPTION_FILTER_RUNTIMES)); - } + extools.set_break_on_runtime( + params + .filters + .iter() + .any(|x| x == EXCEPTION_FILTER_RUNTIMES), + ); + }, DebugClient::Auxtools(auxtools) => { - auxtools.set_catch_runtimes(params.filters.iter().any(|x| x == EXCEPTION_FILTER_RUNTIMES))?; - } + auxtools.set_catch_runtimes( + params + .filters + .iter() + .any(|x| x == EXCEPTION_FILTER_RUNTIMES), + )?; + }, } + Ok(()) } - on ExceptionInfo(&mut self, _params) { + fn ExceptionInfo(&mut self, _params: P) -> R { match &mut self.client { DebugClient::Extools(extools) => { let extools = extools.get()?; // VSC shows exceptionId, description, stackTrace in that order. let message = extools.last_error_message(); - ExceptionInfoResponse { + Ok(ExceptionInfoResponse { exceptionId: message.unwrap_or_default().to_owned(), description: None, breakMode: ExceptionBreakMode::Always, details: None, - } - } + }) + }, - DebugClient::Auxtools(auxtools) => { - ExceptionInfoResponse { - exceptionId: auxtools.get_last_error_message(), - description: None, - breakMode: ExceptionBreakMode::Always, - details: None, - } - } + DebugClient::Auxtools(auxtools) => Ok(ExceptionInfoResponse { + exceptionId: auxtools.get_last_error_message(), + description: None, + breakMode: ExceptionBreakMode::Always, + details: None, + }), } } - on Evaluate(&mut self, params) { - self.evaluate(params)? + fn Evaluate(&mut self, params: P) -> R { + self.evaluate(params) } - on Source(&mut self, params) { + fn Source(&mut self, params: P) -> R { let mut source_reference = params.sourceReference; if let Some(source) = params.source { if let Some(reference) = source.sourceReference { @@ -1380,18 +1516,19 @@ handle_request! { } if let Some(info) = self.stddef_dm_info.as_ref() { - SourceResponse::from(info.text.clone()) + Ok(SourceResponse::from(info.text.clone())) } else { - return Err(Box::new(GenericError("stddef.dm not available"))); + Err(Box::new(GenericError("stddef.dm not available"))) } } - on Disassemble(&mut self, params) { + fn Disassemble(&mut self, params: P) -> R { match &mut self.client { DebugClient::Extools(extools) => { - guard!(let Some(captures) = MEMORY_REFERENCE_REGEX.captures(¶ms.memoryReference) else { + let Some(captures) = MEMORY_REFERENCE_REGEX.captures(¶ms.memoryReference) + else { return Err(Box::new(GenericError("Invalid memory reference"))); - }); + }; let proc = &captures[1]; let override_id: usize = captures[2].parse()?; //let offset: i64 = captures[3].parse()?; @@ -1403,18 +1540,18 @@ handle_request! { address: format!("{}#{}@{}", proc, override_id, instr.offset), instructionBytes: Some(instr.bytes.clone()), instruction: format!("{} {}", instr.mnemonic, instr.comment), - .. Default::default() + ..Default::default() }); } - DisassembleResponse { - instructions: result - } - } + Ok(DisassembleResponse { + instructions: result, + }) + }, DebugClient::Auxtools(_) => { - return Err(Box::new(GenericError("auxtools can't disassemble yet"))); - } + Err(Box::new(GenericError("auxtools can't disassemble yet"))) + }, } } } @@ -1425,6 +1562,7 @@ lazy_static! { } #[derive(Default, Debug)] +#[allow(dead_code)] struct ClientCaps { lines_start_at_1: bool, columns_start_at_1: bool, @@ -1489,6 +1627,7 @@ impl SequenceNumber { }) } + #[allow(dead_code)] fn eprintln>(&self, output: S) { let mut output = output.into(); output.push('\n'); @@ -1567,6 +1706,7 @@ impl Request for AttachVsc { #[derive(Deserialize)] pub struct AttachRequestArgumentsVsc { #[serde(flatten)] + #[allow(dead_code)] base: AttachRequestArguments, port: Option, } diff --git a/crates/dm-langserver/src/document.rs b/crates/dm-langserver/src/document.rs index 02816d2b..005c224f 100644 --- a/crates/dm-langserver/src/document.rs +++ b/crates/dm-langserver/src/document.rs @@ -2,30 +2,32 @@ //! language server protocol. #![allow(dead_code)] -use std::io::{self, Read, BufRead}; +use foldhash::HashMap; use std::borrow::Cow; -use std::collections::HashMap; +use std::io::{self, BufRead, Read}; use std::rc::Rc; use url::Url; -use jsonrpc; -use lsp_types::{TextDocumentItem, TextDocumentIdentifier, - VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent}; +use lsp_types::{ + TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem, + VersionedTextDocumentIdentifier, +}; use super::{invalid_request, url_to_path}; -use ahash::RandomState; - /// A store for the contents of currently-open documents, with appropriate /// fallback for documents which are not currently open. #[derive(Default)] pub struct DocumentStore { - map: HashMap, + map: HashMap, } impl DocumentStore { pub fn open(&mut self, doc: TextDocumentItem) -> Result<(), jsonrpc::Error> { - match self.map.insert(doc.uri.clone(), Document::new(doc.version, doc.text)) { + match self + .map + .insert(doc.uri.clone(), Document::new(doc.version, doc.text)) + { None => Ok(()), Some(_) => Err(invalid_request(format!("opened twice: {}", doc.uri))), } @@ -34,7 +36,10 @@ impl DocumentStore { pub fn close(&mut self, id: TextDocumentIdentifier) -> Result { match self.map.remove(&id.uri) { Some(_) => Ok(id.uri), - None => Err(invalid_request(format!("cannot close non-opened: {}", id.uri))), + None => Err(invalid_request(format!( + "cannot close non-opened: {}", + id.uri + ))), } } @@ -43,24 +48,26 @@ impl DocumentStore { doc_id: VersionedTextDocumentIdentifier, changes: Vec, ) -> Result { - // "If a versioned text document identifier is sent from the server to - // the client and the file is not open in the editor (the server has - // not received an open notification before) the server can send `null` - // to indicate that the version is known and the content on disk is the - // truth (as speced with document content ownership)." - let new_version = match doc_id.version { - Some(version) => version, - None => return Err(invalid_request("document version is missing")), - }; + let new_version = doc_id.version; let document = match self.map.get_mut(&doc_id.uri) { Some(doc) => doc, - None => return Err(invalid_request(format!("cannot change non-opened: {}", doc_id.uri))), + None => { + return Err(invalid_request(format!( + "cannot change non-opened: {}", + doc_id.uri + ))) + }, }; if new_version < document.version { - eprintln!("new_version: {} < document_version: {}", new_version, document.version); - return Err(invalid_request("document version numbers shouldn't go backwards")); + eprintln!( + "new_version: {} < document_version: {}", + new_version, document.version + ); + return Err(invalid_request( + "document version numbers shouldn't go backwards", + )); } document.version = new_version; @@ -86,8 +93,10 @@ impl DocumentStore { return Ok(Cow::Owned(text)); } - Err(io::Error::new(io::ErrorKind::NotFound, - format!("URL not opened and schema is not 'file': {}", url))) + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("URL not opened and schema is not 'file': {url}"), + )) } pub fn read(&self, url: &Url) -> io::Result> { @@ -100,19 +109,21 @@ impl DocumentStore { return Ok(Box::new(file) as Box); } - Err(io::Error::new(io::ErrorKind::NotFound, - format!("URL not opened and schema is not 'file': {}", url))) + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("URL not opened and schema is not 'file': {url}"), + )) } } /// The internal representation of document contents received from the client. struct Document { - version: i64, + version: i32, text: Rc, } impl Document { - fn new(version: i64, text: String) -> Document { + fn new(version: i32, text: String) -> Document { Document { version, text: Rc::new(text), @@ -128,7 +139,7 @@ impl Document { // considered to be the full content of the document." self.text = Rc::new(change.text); return Ok(()); - } + }, }; let start_pos = total_offset(&self.text, range.start.line, range.start.character)?; @@ -140,7 +151,7 @@ impl Document { /// Find the offset into the given text at which the given zero-indexed line /// number begins. -fn line_offset(text: &str, line_number: u64) -> Result { +fn line_offset(text: &str, line_number: u32) -> Result { // Hopefully this logic isn't too far off. let mut start_pos = 0; for _ in 0..line_number { @@ -152,16 +163,16 @@ fn line_offset(text: &str, line_number: u64) -> Result { Ok(start_pos) } -fn total_offset(text: &str, line: u64, mut character: u64) -> Result { +fn total_offset(text: &str, line: u32, mut character: u32) -> Result { let start = line_offset(text, line)?; // column is measured in UTF-16 code units, which is really inconvenient. let mut chars = text[start..].chars(); while character > 0 { if let Some(ch) = chars.next() { - character = character.saturating_sub(ch.len_utf16() as u64); + character = character.saturating_sub(ch.len_utf16() as u32); } else { - break + break; } } Ok(text.len() - chars.as_str().len()) @@ -181,17 +192,17 @@ pub fn offset_to_position(text: &str, offset: usize) -> lsp_types::Position { let mut character = 0; for ch in text[line_start..offset].chars() { - character += ch.len_utf16() as u64; + character += ch.len_utf16() as u32; } lsp_types::Position { line, character } } pub fn get_range(text: &str, range: lsp_types::Range) -> Result<&str, jsonrpc::Error> { - Ok(&text[ - total_offset(text, range.start.line, range.start.character)? - ..total_offset(text, range.end.line, range.end.character)? - ]) + Ok( + &text[total_offset(text, range.start.line, range.start.character)? + ..total_offset(text, range.end.line, range.end.character)?], + ) } pub fn find_word(text: &str, offset: usize) -> &str { @@ -202,7 +213,7 @@ pub fn find_word(text: &str, offset: usize) -> &str { while !text.is_char_boundary(start_next) { start_next -= 1; } - if !text[start_next..start].chars().next().map_or(false, is_ident) { + if !text[start_next..start].chars().next().is_some_and(is_ident) { break; } start = start_next; @@ -215,7 +226,7 @@ pub fn find_word(text: &str, offset: usize) -> &str { while !text.is_char_boundary(end_next) { end_next += 1; } - if !text[end..end_next].chars().next().map_or(false, is_ident) { + if !text[end..end_next].chars().next().is_some_and(is_ident) { break; } end = end_next; @@ -229,7 +240,7 @@ pub fn find_word(text: &str, offset: usize) -> &str { } fn is_ident(ch: char) -> bool { - (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' + ch.is_ascii_digit() || ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_' } /// An adaptation of `std::io::Cursor` which works on an `Rc`, which diff --git a/crates/dm-langserver/src/extras.rs b/crates/dm-langserver/src/extras.rs index cc7022ca..180c3430 100644 --- a/crates/dm-langserver/src/extras.rs +++ b/crates/dm-langserver/src/extras.rs @@ -1,8 +1,10 @@ //! Extensions to the language server protocol. -use lsp_types::SymbolKind; +use foldhash::HashMap; + use lsp_types::notification::*; use lsp_types::request::*; +use lsp_types::SymbolKind; pub enum WindowStatus {} impl Notification for WindowStatus { @@ -63,6 +65,7 @@ impl Request for StartDebugger { #[derive(Debug, Serialize, Deserialize)] pub struct StartDebuggerParams { pub dreamseeker_exe: String, + pub env: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct StartDebuggerResult { diff --git a/crates/dm-langserver/src/find_references.rs b/crates/dm-langserver/src/find_references.rs index e138f8a5..8f3c4001 100644 --- a/crates/dm-langserver/src/find_references.rs +++ b/crates/dm-langserver/src/find_references.rs @@ -1,15 +1,13 @@ //! The symbol table used for "Find References" support. -use std::collections::HashMap; +use foldhash::{HashMap, HashMapExt}; -use dm::Location; -use dm::objtree::*; use dm::ast::*; - -use ahash::RandomState; +use dm::objtree::*; +use dm::Location; pub struct ReferencesTable { - uses: HashMap, + uses: HashMap, symbols: SymbolIdSource, } @@ -22,16 +20,19 @@ struct References { impl ReferencesTable { pub fn new(objtree: &ObjectTree) -> Self { let mut tab = ReferencesTable { - uses: HashMap::with_hasher(RandomState::default()), + uses: HashMap::new(), symbols: SymbolIdSource::new(SymbolIdCategory::LocalVars), }; // Insert the "definition" locations for the types and such objtree.root().recurse(&mut |ty| { - tab.uses.insert(ty.id, References { - references: vec![], - implementations: vec![ty.location], - }); + tab.uses.insert( + ty.id, + References { + references: vec![], + implementations: vec![ty.location], + }, + ); for (name, var) in ty.vars.iter() { if let Some(decl) = ty.get_var_declaration(name) { tab.impl_symbol(decl.id, var.value.location); @@ -49,7 +50,9 @@ impl ReferencesTable { if let Some(ref expr) = var.value.expression { let mut walk = WalkProc::from_ty(&mut tab, objtree, ty); let type_hint = match ty.get_var_declaration(name) { - Some(decl) => walk.static_type(decl.location, &decl.var_type.type_path).basic_type(), + Some(decl) => walk + .static_type(decl.location, &decl.var_type.type_path) + .basic_type(), None => None, }; walk.visit_expression(var.value.location, expr, type_hint); @@ -88,19 +91,30 @@ impl ReferencesTable { fn new_symbol(&mut self, location: Location) -> SymbolId { let id = self.symbols.allocate(); - self.uses.insert(id, References { - references: vec![location], - implementations: vec![], - }); + self.uses.insert( + id, + References { + references: vec![location], + implementations: vec![], + }, + ); id } fn use_symbol(&mut self, symbol: SymbolId, location: Location) { - self.uses.entry(symbol).or_default().references.push(location); + self.uses + .entry(symbol) + .or_default() + .references + .push(location); } fn impl_symbol(&mut self, symbol: SymbolId, location: Location) { - self.uses.entry(symbol).or_default().implementations.push(location); + self.uses + .entry(symbol) + .or_default() + .implementations + .push(location); } } @@ -134,35 +148,50 @@ struct WalkProc<'o> { objtree: &'o ObjectTree, ty: TypeRef<'o>, proc: Option>, - local_vars: HashMap, RandomState>, + local_vars: HashMap>, } impl<'o> WalkProc<'o> { fn from_proc(tab: &'o mut ReferencesTable, objtree: &'o ObjectTree, proc: ProcRef<'o>) -> Self { - let mut local_vars = HashMap::with_hasher(RandomState::default()); - local_vars.insert("global".to_owned(), Local { - ty: StaticType::Type(objtree.root()), - symbol: objtree.root().id, - }); - local_vars.insert(".".to_owned(), Local { - ty: StaticType::None, - symbol: tab.new_symbol(proc.location), - }); - local_vars.insert("args".to_owned(), Local { - ty: StaticType::Type(objtree.expect("/list")), - symbol: tab.new_symbol(proc.location), - }); - local_vars.insert("usr".to_owned(), Local { - ty: StaticType::Type(objtree.expect("/mob")), - symbol: tab.new_symbol(proc.location), - }); + let mut local_vars = HashMap::new(); + local_vars.insert( + "global".to_owned(), + Local { + ty: StaticType::Type(objtree.root()), + symbol: objtree.root().id, + }, + ); + local_vars.insert( + ".".to_owned(), + Local { + ty: StaticType::None, + symbol: tab.new_symbol(proc.location), + }, + ); + local_vars.insert( + "args".to_owned(), + Local { + ty: StaticType::Type(objtree.expect("/list")), + symbol: tab.new_symbol(proc.location), + }, + ); + local_vars.insert( + "usr".to_owned(), + Local { + ty: StaticType::Type(objtree.expect("/mob")), + symbol: tab.new_symbol(proc.location), + }, + ); let ty = proc.ty(); if !ty.is_root() { - local_vars.insert("src".to_owned(), Local { - ty: StaticType::Type(ty), - symbol: tab.new_symbol(proc.location), - }); + local_vars.insert( + "src".to_owned(), + Local { + ty: StaticType::Type(ty), + symbol: tab.new_symbol(proc.location), + }, + ); } WalkProc { @@ -170,23 +199,26 @@ impl<'o> WalkProc<'o> { objtree, ty: proc.ty(), proc: Some(proc), - local_vars + local_vars, } } fn from_ty(tab: &'o mut ReferencesTable, objtree: &'o ObjectTree, ty: TypeRef<'o>) -> Self { - let mut local_vars = HashMap::with_hasher(RandomState::default()); - local_vars.insert("global".to_owned(), Local { - ty: StaticType::Type(objtree.root()), - symbol: objtree.root().id, - }); + let mut local_vars = HashMap::new(); + local_vars.insert( + "global".to_owned(), + Local { + ty: StaticType::Type(objtree.root()), + symbol: objtree.root().id, + }, + ); WalkProc { tab, objtree, ty, proc: None, - local_vars + local_vars, } } @@ -197,10 +229,13 @@ impl<'o> WalkProc<'o> { if let Some(expr) = ¶m.default { self.visit_expression(param.location, expr, None); } - self.local_vars.insert(param.name.to_owned(), Local { - ty, - symbol: self.tab.new_symbol(param.location) - }); + self.local_vars.insert( + param.name.to_owned(), + Local { + ty, + symbol: self.tab.new_symbol(param.location), + }, + ); } self.visit_block(block); } @@ -213,7 +248,9 @@ impl<'o> WalkProc<'o> { fn visit_statement(&mut self, location: Location, statement: &'o Statement) { match statement { - Statement::Expr(expr) => { self.visit_expression(location, expr, None); }, + Statement::Expr(expr) => { + self.visit_expression(location, expr, None); + }, Statement::Return(expr) => { let dot = self.local_vars.get(".").unwrap().symbol; self.tab.use_symbol(dot, location); @@ -221,7 +258,9 @@ impl<'o> WalkProc<'o> { self.visit_expression(location, expr, None); } }, - Statement::Throw(expr) => { self.visit_expression(location, expr, None); }, + Statement::Throw(expr) => { + self.visit_expression(location, expr, None); + }, Statement::While { condition, block } => { self.visit_expression(location, condition, None); self.visit_block(block); @@ -241,8 +280,13 @@ impl<'o> WalkProc<'o> { }, Statement::ForInfinite { block } => { self.visit_block(block); - } - Statement::ForLoop { init, test, inc, block } => { + }, + Statement::ForLoop { + init, + test, + inc, + block, + } => { if let Some(init) = init { self.visit_statement(location, init); } @@ -255,7 +299,13 @@ impl<'o> WalkProc<'o> { self.visit_block(block); }, Statement::ForList(for_list) => { - let ForListStatement { var_type, name, in_list, block, .. } = &**for_list; + let ForListStatement { + var_type, + name, + in_list, + block, + .. + } = &**for_list; if let Some(in_list) = in_list { self.visit_expression(location, in_list, None); } @@ -265,7 +315,14 @@ impl<'o> WalkProc<'o> { self.visit_block(block); }, Statement::ForRange(for_range) => { - let ForRangeStatement { var_type, name, start, end, step, block } = &**for_range; + let ForRangeStatement { + var_type, + name, + start, + end, + step, + block, + } = &**for_range; self.visit_expression(location, end, None); if let Some(step) = step { self.visit_expression(location, step, None); @@ -288,16 +345,22 @@ impl<'o> WalkProc<'o> { } self.visit_block(block); }, - Statement::Switch { input, cases, default } => { + Statement::Switch { + input, + cases, + default, + } => { self.visit_expression(location, input, None); for (case, ref block) in cases.iter() { for case_part in case.elem.iter() { match case_part { - dm::ast::Case::Exact(expr) => { self.visit_expression(case.location, expr, None); }, + dm::ast::Case::Exact(expr) => { + self.visit_expression(case.location, expr, None); + }, dm::ast::Case::Range(start, end) => { self.visit_expression(case.location, start, None); self.visit_expression(case.location, end, None); - } + }, } } self.visit_block(block); @@ -306,16 +369,20 @@ impl<'o> WalkProc<'o> { self.visit_block(default); } }, - Statement::TryCatch { try_block, catch_params, catch_block } => { + Statement::TryCatch { + try_block, + catch_params, + catch_block, + } => { self.visit_block(try_block); for caught in catch_params.iter() { let (var_name, mut type_path) = match caught.split_last() { Some(x) => x, - None => continue + None => continue, }; match type_path.split_first() { Some((first, rest)) if first == "var" => type_path = rest, - _ => {} + _ => {}, } let var_type: VarType = type_path.iter().map(ToOwned::to_owned).collect(); self.visit_var(location, &var_type, var_name, None); @@ -327,7 +394,34 @@ impl<'o> WalkProc<'o> { Statement::Goto(_) => {}, Statement::Crash(_) => {}, Statement::Label { name: _, block } => self.visit_block(block), - Statement::Del(expr) => { self.visit_expression(location, expr, None); }, + Statement::Del(expr) => { + self.visit_expression(location, expr, None); + }, + Statement::ForKeyValue(for_key_value) => { + let ForKeyValueStatement { + var_type, + key, + key_input_type: _, + value, + in_list, + block, + } = &**for_key_value; + if let Some(in_list) = in_list { + self.visit_expression(location, in_list, None); + } + if let Some(var_type) = var_type { + self.visit_var(location, var_type, key, None); + } + // the "v" in a DM for (var/k, v) statement is essentially typeless. + // There is currently no way to change that. + let var_type_value = VarType { + flags: VarTypeFlags::from_bits_truncate(0), + type_path: Box::new([]), + input_type: InputType::from_bits_truncate(0), + }; + self.visit_var(location, &var_type_value, value, None); + self.visit_block(block); + }, } } @@ -335,16 +429,25 @@ impl<'o> WalkProc<'o> { self.visit_var(location, &var.var_type, &var.name, var.value.as_ref()) } - fn visit_var(&mut self, location: Location, var_type: &VarType, name: &str, value: Option<&'o Expression>) { + fn visit_var( + &mut self, + location: Location, + var_type: &VarType, + name: &str, + value: Option<&'o Expression>, + ) { let ty = self.static_type(location, &var_type.type_path); self.use_type(location, &ty); - if let Some(ref expr) = value { + if let Some(expr) = value { self.visit_expression(location, expr, ty.basic_type()); } - self.local_vars.insert(name.to_owned(), Local { - ty, - symbol: self.tab.new_symbol(location), - }); + self.local_vars.insert( + name.to_owned(), + Local { + ty, + symbol: self.tab.new_symbol(location), + }, + ); } fn use_type(&mut self, location: Location, ty: &StaticType<'o>) { @@ -354,25 +457,31 @@ impl<'o> WalkProc<'o> { StaticType::List { list, keys } => { self.tab.use_symbol(list.id, location); self.use_type(location, keys); - } + }, } } - fn visit_expression(&mut self, location: Location, expression: &'o Expression, type_hint: Option>) -> StaticType<'o> { + #[allow(clippy::only_used_in_recursion)] + fn visit_expression( + &mut self, + location: Location, + expression: &'o Expression, + type_hint: Option>, + ) -> StaticType<'o> { match expression { Expression::Base { term, follow } => { - let base_type_hint = if follow.is_empty() { - type_hint - } else { - None - }; + let base_type_hint = if follow.is_empty() { type_hint } else { None }; let mut ty = self.visit_term(term.location, &term.elem, base_type_hint); for each in follow.iter() { ty = self.visit_follow(each.location, ty, &each.elem); } ty }, - Expression::BinaryOp { op: BinaryOp::Or, lhs, rhs } => { + Expression::BinaryOp { + op: BinaryOp::Or, + lhs, + rhs, + } => { // It appears that DM does this in more cases than this, but // this is the only case I've seen it used in the wild. // ex: var/datum/cache_entry/E = cache[key] || new @@ -395,7 +504,7 @@ impl<'o> WalkProc<'o> { let ty = self.visit_expression(location, if_, type_hint); self.visit_expression(location, else_, type_hint); ty - } + }, } } @@ -412,7 +521,12 @@ impl<'o> WalkProc<'o> { } } - fn visit_term(&mut self, location: Location, term: &'o Term, type_hint: Option>) -> StaticType<'o> { + fn visit_term( + &mut self, + location: Location, + term: &'o Term, + type_hint: Option>, + ) -> StaticType<'o> { match term { Term::Null => StaticType::None, Term::Int(_) => StaticType::None, @@ -464,9 +578,7 @@ impl<'o> WalkProc<'o> { } }, - Term::NewImplicit { args } => { - self.visit_new(location, type_hint, args) - }, + Term::NewImplicit { args } => self.visit_new(location, type_hint, args), Term::NewPrefab { prefab, args } => { let typepath = self.visit_prefab(location, prefab); self.visit_new(location, typepath, args) @@ -496,7 +608,11 @@ impl<'o> WalkProc<'o> { } StaticType::None }, - Term::Input { args, input_type: _, in_list } => { + Term::Input { + args, + input_type: _, + in_list, + } => { // TODO: use /proc/input self.visit_arguments(location, args); if let Some(ref expr) = in_list { @@ -520,10 +636,64 @@ impl<'o> WalkProc<'o> { self.visit_arguments(location, args_2); StaticType::None }, + Term::ExternalCall { + library, + function, + args, + } => { + if let Some(library) = library { + self.visit_expression(location, library, None); + } + self.visit_expression(location, function, None); + self.visit_arguments(location, args); + StaticType::None + }, + + Term::GlobalCall(name, args) => { + if let Some(proc) = self.objtree.root().get_proc(name) { + self.visit_call(location, self.objtree.root(), proc, args, false) + } else { + self.visit_arguments(location, args); + StaticType::None + } + }, + Term::GlobalIdent(name) => { + if let Some(decl) = self.objtree.root().get_var_declaration(name) { + self.tab.use_symbol(decl.id, location); + self.static_type(location, &decl.var_type.type_path) + } else { + StaticType::None + } + }, + Term::__TYPE__ => { + self.tab.use_symbol(self.ty.id, location); + StaticType::None + }, + Term::__PROC__ => { + let Some(proc) = self.proc else { + return StaticType::None; + }; + if let Some(decl) = self.ty.get_proc_declaration(proc.name()) { + self.tab.use_symbol(decl.id, location); + } + StaticType::None + }, + Term::__IMPLIED_TYPE__ => { + let Some(implied_type) = type_hint else { + return StaticType::None; + }; + self.tab.use_symbol(implied_type.id, location); + StaticType::Type(implied_type) + }, } } - fn visit_new(&mut self, location: Location, typepath: Option>, args: &'o Option>) -> StaticType<'o> { + fn visit_new( + &mut self, + location: Location, + typepath: Option>, + args: &'o Option>, + ) -> StaticType<'o> { if let Some(typepath) = typepath { if let Some(new_proc) = typepath.get_proc("New") { self.visit_call( @@ -533,7 +703,8 @@ impl<'o> WalkProc<'o> { args.as_ref().map_or(&[], |v| &v[..]), // New calls are exact: `new /datum()` will always call // `/datum/New()` and never an override. - true); + true, + ); } // If we had a diagnostic context here, we'd error for // types other than `/list`, which has no `New()`. @@ -558,7 +729,9 @@ impl<'o> WalkProc<'o> { let mut type_hint = None; if let Some(decl) = nav.ty().get_var_declaration(key) { self.tab.use_symbol(decl.id, location); - type_hint = self.static_type(location, &decl.var_type.type_path).basic_type(); + type_hint = self + .static_type(location, &decl.var_type.type_path) + .basic_type(); } self.visit_expression(location, expr, type_hint); } @@ -569,7 +742,12 @@ impl<'o> WalkProc<'o> { } } - fn visit_field(&mut self, location: Location, lhs: StaticType<'o>, name: &'o str) -> StaticType<'o> { + fn visit_field( + &mut self, + location: Location, + lhs: StaticType<'o>, + name: &'o str, + ) -> StaticType<'o> { if let Some(ty) = lhs.basic_type() { if let Some(decl) = ty.get_var_declaration(name) { self.tab.use_symbol(decl.id, location); @@ -582,7 +760,12 @@ impl<'o> WalkProc<'o> { } } - fn visit_follow(&mut self, location: Location, lhs: StaticType<'o>, rhs: &'o Follow) -> StaticType<'o> { + fn visit_follow( + &mut self, + location: Location, + lhs: StaticType<'o>, + rhs: &'o Follow, + ) -> StaticType<'o> { match rhs { Follow::Unary(op) => self.visit_unary(lhs, *op), Follow::Index(_, expr) => { @@ -595,6 +778,7 @@ impl<'o> WalkProc<'o> { } }, Follow::Field(_, name) => self.visit_field(location, lhs, name), + Follow::StaticField(name) => self.visit_field(location, lhs, name), Follow::Call(_, name, arguments) => { if let Some(ty) = lhs.basic_type() { if let Some(proc) = ty.get_proc(name) { @@ -608,6 +792,14 @@ impl<'o> WalkProc<'o> { StaticType::None } }, + Follow::ProcReference(name) => { + if let Some(ty) = lhs.basic_type() { + if let Some(decl) = ty.get_proc_declaration(name) { + self.tab.use_symbol(decl.id, location); + } + } + StaticType::None + }, } } @@ -616,12 +808,24 @@ impl<'o> WalkProc<'o> { StaticType::None } - fn visit_binary(&mut self, _lhs: StaticType<'o>, _rhs: StaticType<'o>, _op: BinaryOp) -> StaticType<'o> { + fn visit_binary( + &mut self, + _lhs: StaticType<'o>, + _rhs: StaticType<'o>, + _op: BinaryOp, + ) -> StaticType<'o> { // TODO: mark usage of operatorX procs StaticType::None } - fn visit_call(&mut self, location: Location, src: TypeRef<'o>, proc: ProcRef, args: &'o [Expression], is_exact: bool) -> StaticType<'o> { + fn visit_call( + &mut self, + location: Location, + src: TypeRef<'o>, + proc: ProcRef, + args: &'o [Expression], + is_exact: bool, + ) -> StaticType<'o> { // register use of symbol if !is_exact { // Only include uses of the symbol by name, not `.()` or `..()` @@ -634,17 +838,21 @@ impl<'o> WalkProc<'o> { // identify and register kwargs used for arg in args { let mut argument_value = arg; - if let Expression::AssignOp { op: AssignOp::Assign, lhs, rhs } = arg { + if let Expression::AssignOp { + op: AssignOp::Assign, + lhs, + rhs, + } = arg + { match lhs.as_term() { - Some(Term::Ident(_name)) | - Some(Term::String(_name)) => { + Some(Term::Ident(_name)) | Some(Term::String(_name)) => { // Don't visit_expression the kwarg key. argument_value = rhs; // TODO: register a usage of the kwarg symbol here. // Recurse to children too? - } - _ => {} + }, + _ => {}, } } @@ -657,14 +865,18 @@ impl<'o> WalkProc<'o> { fn visit_arguments(&mut self, location: Location, args: &'o [Expression]) { for arg in args { let mut argument_value = arg; - if let Expression::AssignOp { op: AssignOp::Assign, lhs, rhs } = arg { + if let Expression::AssignOp { + op: AssignOp::Assign, + lhs, + rhs, + } = arg + { match lhs.as_term() { - Some(Term::Ident(_name)) | - Some(Term::String(_name)) => { + Some(Term::Ident(_name)) | Some(Term::String(_name)) => { // Don't visit_expression the kwarg key. argument_value = rhs; - } - _ => {} + }, + _ => {}, } } @@ -672,8 +884,21 @@ impl<'o> WalkProc<'o> { } } + #[allow(clippy::only_used_in_recursion)] fn static_type(&mut self, location: Location, mut of: &[String]) -> StaticType<'o> { - while !of.is_empty() && ["static", "global", "const", "tmp"].contains(&&*of[0]) { + while !of.is_empty() + && [ + "static", + "global", + "const", + "tmp", + "final", + "SpacemanDMM_final", + "SpacemanDMM_private", + "SpacemanDMM_protected", + ] + .contains(&&*of[0]) + { of = &of[1..]; } diff --git a/crates/dm-langserver/src/jrpc_io.rs b/crates/dm-langserver/src/jrpc_io.rs index 8cd378d2..ff83caef 100644 --- a/crates/dm-langserver/src/jrpc_io.rs +++ b/crates/dm-langserver/src/jrpc_io.rs @@ -28,14 +28,14 @@ fn read(input: &mut R) -> Result, Box = buffer.split(' ').collect(); if parts.len() != 2 { - eprintln!("JSON-RPC read error: parts.len() != 2\n{:?}", parts); + eprintln!("JSON-RPC read error: parts.len() != 2\n{parts:?}"); return Ok(None); } if !parts[0].eq_ignore_ascii_case("content-length:") { - eprintln!("JSON-RPC read error: !parts[0].eq_ignore_ascii_case(\"content-length:\")\n{:?}", parts); + eprintln!("JSON-RPC read error: !parts[0].eq_ignore_ascii_case(\"content-length:\")\n{parts:?}"); return Ok(None); } - usize::from_str_radix(parts[1].trim(), 10)? + parts[1].trim().parse::()? }; // skip blank line diff --git a/crates/dm-langserver/src/macros.rs b/crates/dm-langserver/src/macros.rs index 31785c05..56d74364 100644 --- a/crates/dm-langserver/src/macros.rs +++ b/crates/dm-langserver/src/macros.rs @@ -1,135 +1,4 @@ //! Utility macros. - -pub mod all_methods { - pub use lsp_types::request::*; - pub use crate::extras::*; -} - -pub mod all_notifications { - pub use lsp_types::notification::*; - pub use crate::extras::*; -} - -macro_rules! handle_method_call { - ($($(#[$attr:meta])* on $what:ident(&mut $self:ident, $p:pat) $b:block)*) => { - impl<'a> Engine<'a> { - fn handle_method_call_table(method: &str) -> Option Result> { - use macros::all_methods::*; - $(if method == <$what>::METHOD { - Some(|this, params_value| { - let params: <$what as Request>::Params = serde_json::from_value(params_value).map_err(invalid_request)?; - let result: <$what as Request>::Result = this.$what(params)?; - Ok(serde_json::to_value(result).expect("encode problem")) - }) - } else)* { - None - } - } - - $( - #[allow(non_snake_case)] - $(#[$attr])* - fn $what(&mut $self, $p: ::Params) - -> Result<::Result, jsonrpc::Error> - { - #[allow(unused_imports)] - use lsp_types::*; - #[allow(unused_imports)] - use lsp_types::request::*; - let _v = $b; - #[allow(unreachable_code)] { Ok(_v) } - } - )* - } - } -} - -macro_rules! handle_notification { - ($(on $what:ident(&mut $self:ident, $p:pat) $b:block)*) => { - impl<'a> Engine<'a> { - fn handle_notification_table(method: &str) -> Option Result<(), jsonrpc::Error>> { - use macros::all_notifications::*; - $(if method == <$what>::METHOD { - Some(|this, params_value| { - let params: <$what as Notification>::Params = serde_json::from_value(params_value).map_err(invalid_request)?; - this.$what(params) - }) - } else)* { - None - } - } - - $( - #[allow(non_snake_case)] - fn $what(&mut $self, $p: ::Params) - -> Result<(), jsonrpc::Error> - { - #[allow(unused_imports)] - use lsp_types::*; - #[allow(unused_imports)] - use lsp_types::notification::*; - let _v = $b; - #[allow(unreachable_code)] { Ok(_v) } - } - )* - } - } -} - -macro_rules! handle_request { - ($(on $what:ident(&mut $self:ident, $p:pat) $b:block)*) => { - impl Debugger { - fn handle_request_table(command: &str) -> Option Result>> { - use dap_types::*; - $(if command == <$what>::COMMAND { - Some(|this, arguments| { - let params: <$what as Request>::Params = serde_json::from_value(arguments)?; - let result: <$what as Request>::Result = this.$what(params)?; - Ok(serde_json::to_value(result).expect("encode problem")) - }) - } else)* { - None - } - } - - $( - #[allow(non_snake_case)] - fn $what(&mut $self, $p: <$what as dap_types::Request>::Params) - -> Result<<$what as dap_types::Request>::Result, Box> - { - let _v = $b; - #[allow(unreachable_code)] { Ok(_v) } - } - )* - } - } -} - -macro_rules! handle_extools { - ($(on $what:ident(&mut $self:ident, $p:pat) $b:block)*) => { - impl ExtoolsThread { - fn handle_response_table(type_: &str) -> Option Result<(), Box>> { - $(if type_ == <$what as Response>::TYPE { - Some(|this, content| { - let deserialized: $what = serde_json::from_value(content)?; - this.$what(deserialized) - }) - } else)* { - None - } - } - - $( - #[allow(non_snake_case)] - fn $what(&mut $self, $p: $what) -> Result<(), Box> { - let _v = $b; - #[allow(unreachable_code)] { Ok(_v) } - } - )* - } - } -} - macro_rules! if_annotation { ($p:pat in $a:expr; $b:block) => { for (_, thing) in $a.clone() { @@ -142,10 +11,10 @@ macro_rules! if_annotation { } macro_rules! match_annotation { - ($a:expr; $($($p:pat)|* => $b:block,)*) => { + ($a:expr; $($p:pat => $b:block,)*) => { for (_, thing) in $a.clone() { match thing { - $($($p)|* => $b,)* + $($p => $b,)* _ => {} } } diff --git a/crates/dm-langserver/src/main.rs b/crates/dm-langserver/src/main.rs index 3c133f80..023c454a 100644 --- a/crates/dm-langserver/src/main.rs +++ b/crates/dm-langserver/src/main.rs @@ -7,53 +7,52 @@ //! * https://github.com/rust-lang-nursery/rls #![deny(unsafe_code)] -extern crate url; extern crate serde; extern crate serde_json; -#[macro_use] extern crate serde_derive; -extern crate interval_tree; -extern crate lsp_types; -extern crate jsonrpc_core as jsonrpc; -extern crate dreammaker as dm; +extern crate url; +#[macro_use] +extern crate serde_derive; extern crate dreamchecker; +extern crate dreammaker as dm; +extern crate interval_tree; +extern crate jsonrpc_core as jsonrpc; extern crate libc; -#[macro_use] extern crate guard; +extern crate lsp_types; extern crate regex; -#[macro_use] extern crate lazy_static; +#[macro_use] +extern crate lazy_static; -#[macro_use] mod macros; -mod jrpc_io; -mod document; -mod symbol_search; -mod find_references; -mod extras; -mod completion; -mod color; +#[macro_use] +mod macros; mod background; - +mod color; +mod completion; mod debugger; +mod document; +mod extras; +mod find_references; +mod jrpc_io; +mod symbol_search; -use std::path::PathBuf; -use std::collections::{HashMap, HashSet, VecDeque}; -use std::collections::hash_map::Entry; -use std::sync::{Arc, Mutex}; -use std::rc::Rc; - -use url::Url; -use jsonrpc::{Request, Call, Response, Output}; -use lsp_types::MessageType; - -use dm::FileId; +use crate::extras::{Reparse, StartDebugger}; use dm::annotation::{Annotation, AnnotationTree}; use dm::objtree::TypeRef; - -use ahash::RandomState; +use dm::FileId; +use foldhash::{HashMap, HashMapExt, HashSet, HashSetExt}; +use jsonrpc::{Call, Output, Response}; +use lsp_types::{notification::*, request::*, *}; +use std::collections::hash_map::Entry; +use std::collections::VecDeque; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use url::Url; fn main() { std::env::set_var("RUST_BACKTRACE", "1"); eprintln!( - "dm-langserver {} Copyright (C) 2017-2021 Tad Hardesty", + "dm-langserver {} Copyright (C) 2017-2025 Tad Hardesty", env!("CARGO_PKG_VERSION") ); eprintln!("This program comes with ABSOLUTELY NO WARRANTY. This is free software,"); @@ -62,34 +61,41 @@ fn main() { eprintln!(); match std::env::current_exe() { Ok(path) => eprintln!("executable: {}", path.display()), - Err(e) => eprintln!("exe check failure: {}", e), + Err(e) => eprintln!("exe check failure: {e}"), } - eprint!("{}", include_str!(concat!(env!("OUT_DIR"), "/build-info.txt"))); - #[cfg(extools_bundle)] { - eprintln!("extools commit: {}", env!("EXTOOLS_COMMIT_HASH")); + eprint!( + "{}", + include_str!(concat!(env!("OUT_DIR"), "/build-info.txt")) + ); + #[cfg(extools_bundle)] + { + eprintln!("extools commit: {}", env!("BUNDLE_VERSION_extools.dll")); } - #[cfg(auxtools_bundle)] { - eprintln!("auxtools commit: {}", env!("AUXTOOLS_COMMIT_HASH")); + #[cfg(auxtools_bundle)] + { + eprintln!( + "auxtools commit: {}", + env!("BUNDLE_VERSION_debug_server.dll") + ); } match std::env::current_dir() { Ok(path) => eprintln!("directory: {}", path.display()), - Err(e) => eprintln!("dir check failure: {}", e), + Err(e) => eprintln!("dir check failure: {e}"), } let mut args = std::env::args(); - let _ = args.next(); // skip executable name + let _ = args.next(); // skip executable name if let Some(arg) = args.next() { if arg == "--debugger" { return debugger::debugger_main(args); } else if arg == "--version" { return; } else { - panic!("unknown argument {:?}", arg); + panic!("unknown argument {arg:?}"); } } - let context = dm::Context::default(); - let mut engine = Engine::new(&context); + let mut engine = Engine::new(); jrpc_io::run_until_stdin_eof(|message| engine.handle_input(message)); engine.exit(0); } @@ -118,8 +124,12 @@ impl ClientCaps { if let Some(ref text_document) = caps.text_document { if let Some(ref signature_help) = text_document.signature_help { if let Some(ref signature_information) = signature_help.signature_information { - if let Some(ref parameter_information) = signature_information.parameter_information { - if let Some(label_offset_support) = parameter_information.label_offset_support { + if let Some(ref parameter_information) = + signature_information.parameter_information + { + if let Some(label_offset_support) = + parameter_information.label_offset_support + { this.label_offset_support = label_offset_support; } } @@ -133,8 +143,8 @@ impl ClientCaps { } } if let Some(ref experimental) = caps.experimental { - if let Some(ref dreammaker) = experimental.get("dreammaker") { - if let Some(ref object_tree) = dreammaker.get("objectTree") { + if let Some(dreammaker) = experimental.get("dreammaker") { + if let Some(object_tree) = dreammaker.get("objectTree") { if let Some(value) = object_tree.as_bool() { this.object_tree = value; } @@ -147,21 +157,26 @@ impl ClientCaps { #[derive(Default)] struct DiagnosticsTracker { - sent: HashSet, + sent: HashSet, } impl DiagnosticsTracker { fn file_url(root: Option<&Url>, file_list: &dm::FileList, file: dm::FileId) -> Option { let fname_string = file_list.get_path(file).display().to_string(); - if let Some(ref root) = root { + if let Some(root) = root { root.join(&fname_string).ok() } else { Url::parse(&fname_string).ok() } } - fn build(root: Option<&Url>, file_list: &dm::FileList, errors: &[dm::DMError], related_info: bool) -> HashMap, RandomState> { - let mut map: HashMap<_, Vec<_>, RandomState> = HashMap::with_hasher(RandomState::default()); + fn build( + root: Option<&Url>, + file_list: &dm::FileList, + errors: &[dm::DMError], + related_info: bool, + ) -> HashMap> { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); for error in errors.iter() { let loc = error.location(); let related_information = if !related_info || error.notes().is_empty() { @@ -169,7 +184,11 @@ impl DiagnosticsTracker { } else { let mut notes = Vec::with_capacity(error.notes().len()); for note in error.notes().iter() { - guard!(let Some(uri) = DiagnosticsTracker::file_url(root, file_list, note.location().file) else { continue }); + let Some(uri) = + DiagnosticsTracker::file_url(root, file_list, note.location().file) + else { + continue; + }; notes.push(lsp_types::DiagnosticRelatedInformation { location: lsp_types::Location { uri, @@ -187,37 +206,39 @@ impl DiagnosticsTracker { source: component_to_source(error.component()), code: convert_errorcode(error.errortype()), related_information, - .. Default::default() + ..Default::default() }; - guard!(let Some(uri) = DiagnosticsTracker::file_url(root, file_list, loc.file) else { continue }); - map.entry(uri) - .or_insert_with(Default::default) - .push(diag); + let Some(uri) = DiagnosticsTracker::file_url(root, file_list, loc.file) else { + continue; + }; + map.entry(uri).or_insert_with(Default::default).push(diag); if !related_info { // Fallback in case the client does not support related info for note in error.notes().iter() { let diag = lsp_types::Diagnostic { message: note.description().to_owned(), - severity: Some(lsp_types::DiagnosticSeverity::Information), + severity: Some(lsp_types::DiagnosticSeverity::INFORMATION), range: location_to_range(note.location()), source: component_to_source(error.component()), - .. Default::default() + ..Default::default() }; - guard!(let Some(uri) = DiagnosticsTracker::file_url(root, file_list, note.location().file) else { continue }); - map.entry(uri) - .or_insert_with(Default::default) - .push(diag); + let Some(uri) = + DiagnosticsTracker::file_url(root, file_list, note.location().file) + else { + continue; + }; + map.entry(uri).or_insert_with(Default::default).push(diag); } } } map } - fn send(&mut self, map: HashMap, RandomState>) { - let mut new_sent = HashSet::with_capacity_and_hasher(map.len(), RandomState::default()); + fn send(&mut self, map: HashMap>) { + let mut new_sent = HashSet::with_capacity(map.len()); for (url, diagnostics) in map { - self.sent.remove(&url); // don't erase below + self.sent.remove(&url); // don't erase below new_sent.insert(url.clone()); issue_notification::( lsp_types::PublishDiagnosticsParams { @@ -241,20 +262,20 @@ impl DiagnosticsTracker { } } -struct Engine<'a> { +struct Engine { docs: document::DocumentStore, status: InitStatus, - parent_pid: u64, + parent_pid: u32, threads: Vec>, root: Option, - context: &'a dm::Context, + context: dm::Context, defines: Option, objtree: Arc, references_table: background::Background, - annotations: HashMap), RandomState>, + annotations: HashMap)>, diagnostics_tracker: Arc>, client_caps: ClientCaps, @@ -262,8 +283,8 @@ struct Engine<'a> { debug_server_dll: Option, } -impl<'a> Engine<'a> { - fn new(context: &'a dm::Context) -> Self { +impl Engine { + fn new() -> Self { Engine { docs: Default::default(), @@ -272,7 +293,7 @@ impl<'a> Engine<'a> { threads: Default::default(), root: None, - context, + context: dm::Context::default(), defines: None, objtree: Default::default(), references_table: Default::default(), @@ -297,18 +318,20 @@ impl<'a> Engine<'a> { issue_notification::(params) } - fn show_message(&mut self, typ: MessageType, message: S) where - S: Into + fn show_message(&mut self, typ: MessageType, message: S) + where + S: Into, { let message = message.into(); - eprintln!("{:?}: {}", typ, message); + eprintln!("{typ:?}: {message}"); self.issue_notification::( - lsp_types::ShowMessageParams { typ, message } + lsp_types::ShowMessageParams { typ, message }, ) } - fn show_status(&self, message: S) where - S: Into + fn show_status(&self, message: S) + where + S: Into, { self.issue_notification::(extras::WindowStatusParams { environment: None, @@ -319,10 +342,10 @@ impl<'a> Engine<'a> { fn file_url(&self, file: dm::FileId) -> Result { if let Some(ref root) = self.root { root.join(&self.context.file_path(file).display().to_string()) - .map_err(|e| invalid_request(format!("error in file_url: {}", e))) + .map_err(|e| invalid_request(format!("error in file_url: {e}"))) } else { Url::parse(&self.context.file_path(file).display().to_string()) - .map_err(|e| invalid_request(format!("error in rootless file_url {}", e))) + .map_err(|e| invalid_request(format!("error in rootless file_url {e}"))) } } @@ -336,7 +359,12 @@ impl<'a> Engine<'a> { } } - fn convert_location(&self, loc: dm::Location, docs: &dm::docs::DocCollection, if_builtin: &[&str]) -> Result { + fn convert_location( + &self, + loc: dm::Location, + docs: &dm::docs::DocCollection, + if_builtin: &[&str], + ) -> Result { Ok(lsp_types::Location { uri: if loc.is_builtins() { let temp; @@ -348,7 +376,8 @@ impl<'a> Engine<'a> { temp = if_builtin.join(""); &temp } - )).map_err(invalid_request)? + )) + .map_err(invalid_request)? } else { self.file_url(loc.file)? }, @@ -365,9 +394,7 @@ impl<'a> Engine<'a> { // offload serialization costs to another thread std::thread::spawn(move || { let start = std::time::Instant::now(); - issue_notification::(extras::ObjectTreeParams { - root, - }); + issue_notification::(extras::ObjectTreeParams { root }); let elapsed = start.elapsed(); eprintln!( "serialized objtree in {}.{:03}s", @@ -381,8 +408,10 @@ impl<'a> Engine<'a> { fn recurse_objtree(&self, ty: TypeRef) -> extras::ObjectTreeType { let mut entry = extras::ObjectTreeType { name: ty.name().to_owned(), - kind: lsp_types::SymbolKind::Class, - location: self.convert_location(ty.location, &ty.docs, &[&ty.path]).ok(), + kind: lsp_types::SymbolKind::CLASS, + location: self + .convert_location(ty.location, &ty.docs, &[&ty.path]) + .ok(), vars: Vec::new(), procs: Vec::new(), children: Vec::new(), @@ -393,8 +422,14 @@ impl<'a> Engine<'a> { let is_declaration = var.declaration.is_some(); entry.vars.push(extras::ObjectTreeVar { name: name.to_owned(), - kind: lsp_types::SymbolKind::Field, - location: self.convert_location(var.value.location, &var.value.docs, &[&ty.path, "/var/", name]).ok(), + kind: lsp_types::SymbolKind::FIELD, + location: self + .convert_location( + var.value.location, + &var.value.docs, + &[&ty.path, "/var/", name], + ) + .ok(), is_declaration, }); } @@ -406,8 +441,10 @@ impl<'a> Engine<'a> { for value in proc.value.iter() { entry.procs.push(extras::ObjectTreeProc { name: name.to_owned(), - kind: lsp_types::SymbolKind::Method, - location: self.convert_location(value.location, &value.docs, &[&ty.path, "/proc/", name]).ok(), + kind: lsp_types::SymbolKind::METHOD, + location: self + .convert_location(value.location, &value.docs, &[&ty.path, "/proc/", name]) + .ok(), is_verb, }); is_verb = None; @@ -443,14 +480,17 @@ impl<'a> Engine<'a> { let print_thread_total = move || { let elapsed = original_start.elapsed(); - eprintln!(" - total {}.{:03}s", elapsed.as_secs(), elapsed.subsec_millis()); + eprintln!( + " - total {}.{:03}s", + elapsed.as_secs(), + elapsed.subsec_millis() + ); }; // Set up the preprocessor. - let ctx = self.context; - ctx.reset_io_time(); - ctx.autodetect_config(&environment); - let mut pp = match dm::preprocessor::Preprocessor::new(ctx, environment.clone()) { + self.context.reset_io_time(); + self.context.autodetect_config(&environment); + let mut pp = match dm::preprocessor::Preprocessor::new(&self.context, environment.clone()) { Ok(pp) => pp, Err(err) => { self.issue_notification::( @@ -458,35 +498,50 @@ impl<'a> Engine<'a> { uri: path_to_url(environment)?, diagnostics: vec![lsp_types::Diagnostic { message: err.to_string(), - .. Default::default() + ..Default::default() }], version: None, }, ); - eprintln!("{:?}", err); + eprintln!("{err:?}"); return Ok(()); - } + }, }; - let elapsed = start.elapsed(); start += elapsed; + let elapsed = start.elapsed(); + start += elapsed; if elapsed.as_millis() > 0 { - eprint!("setup {}.{:03}s - ", elapsed.as_secs(), elapsed.subsec_millis()); + eprint!( + "setup {}.{:03}s - ", + elapsed.as_secs(), + elapsed.subsec_millis() + ); } // Parse the environment. let fatal_errored; { - let mut parser = dm::parser::Parser::new(ctx, dm::indents::IndentProcessor::new(ctx, &mut pp)); + let mut parser = dm::parser::Parser::new( + &self.context, + dm::indents::IndentProcessor::new(&self.context, &mut pp), + ); parser.enable_procs(); let (fatal_errored_2, objtree) = parser.parse_object_tree_2(); fatal_errored = fatal_errored_2; self.objtree = Arc::new(objtree); } - let elapsed = start.elapsed(); start += elapsed; + let elapsed = start.elapsed(); + start += elapsed; { - let disk = ctx.get_io_time(); - let parse = elapsed - disk; - eprint!("disk {}.{:03}s - parse {}.{:03}s", disk.as_secs(), disk.subsec_millis(), parse.as_secs(), parse.subsec_millis()); + let disk = self.context.get_io_time(); + let parse = elapsed.saturating_sub(disk); + eprint!( + "disk {}.{:03}s - parse {}.{:03}s", + disk.as_secs(), + disk.subsec_millis(), + parse.as_secs(), + parse.subsec_millis() + ); } // Background thread: prepare the Find All References database. @@ -494,7 +549,11 @@ impl<'a> Engine<'a> { self.references_table.spawn(move || { let table = find_references::ReferencesTable::new(&references_objtree); let elapsed = start.elapsed(); - eprint!("references {}.{:03}s", elapsed.as_secs(), elapsed.subsec_millis()); + eprint!( + "references {}.{:03}s", + elapsed.as_secs(), + elapsed.subsec_millis() + ); print_thread_total(); table }); @@ -503,7 +562,7 @@ impl<'a> Engine<'a> { let mut diagnostics_lock = self.diagnostics_tracker.lock().unwrap(); // Background thread: If enabled, and parse was OK, run dreamchecker. - if ctx.config().langserver.dreamchecker && !fatal_errored { + if self.context.config().langserver.dreamchecker && !fatal_errored { self.show_status("checking"); let context = self.context.clone(); let objtree = self.objtree.clone(); @@ -512,8 +571,13 @@ impl<'a> Engine<'a> { let diagnostics_tracker = self.diagnostics_tracker.clone(); std::thread::spawn(move || { dreamchecker::run(&context, &objtree); - let elapsed = start.elapsed(); start += elapsed; - eprint!("dreamchecker {}.{:03}s", elapsed.as_secs(), elapsed.subsec_millis()); + let elapsed = start.elapsed(); + start += elapsed; + eprint!( + "dreamchecker {}.{:03}s", + elapsed.as_secs(), + elapsed.subsec_millis() + ); print_thread_total(); let map = DiagnosticsTracker::build( @@ -542,14 +606,24 @@ impl<'a> Engine<'a> { self.defines = Some(pp.finalize()); - let elapsed = start.elapsed(); start += elapsed; - eprint!(" - diagnostics {}.{:03}s", elapsed.as_secs(), elapsed.subsec_millis()); + let elapsed = start.elapsed(); + start += elapsed; + eprint!( + " - diagnostics {}.{:03}s", + elapsed.as_secs(), + elapsed.subsec_millis() + ); // If enabled, send the JSON for the object tree panel. if self.client_caps.object_tree { self.update_objtree(); - let elapsed = start.elapsed(); start += elapsed; - eprint!(" - object tree {}.{:03}s", elapsed.as_secs(), elapsed.subsec_millis()); + let elapsed = start.elapsed(); + start += elapsed; + eprint!( + " - object tree {}.{:03}s", + elapsed.as_secs(), + elapsed.subsec_millis() + ); } /*if let Some(objtree) = Arc::get_mut(&mut self.objtree) { @@ -562,7 +636,10 @@ impl<'a> Engine<'a> { Ok(()) } - fn get_annotations(&mut self, url: &Url) -> Result<(FileId, FileId, Rc), jsonrpc::Error> { + fn get_annotations( + &mut self, + url: &Url, + ) -> Result<(FileId, FileId, Rc), jsonrpc::Error> { Ok(match self.annotations.entry(url.to_owned()) { Entry::Occupied(o) => o.get().clone(), Entry::Vacant(v) => match self.root { @@ -580,31 +657,48 @@ impl<'a> Engine<'a> { Ok(path) => path, Err(_) => "".as_ref(), }; - let (real_file_id, mut preprocessor) = match self.context.get_file(&stripped) { + let (real_file_id, mut preprocessor) = match self.context.get_file(stripped) { Some(id) => (id, defines.branch_at_file(id, &self.context)), None => (FileId::default(), defines.branch_at_end(&self.context)), }; let contents = self.docs.read(url).map_err(invalid_request)?; - let file_id = preprocessor.push_file(stripped.to_owned(), contents).map_err(invalid_request)?; + let file_id = preprocessor + .push_file(stripped.to_owned(), contents) + .map_err(invalid_request)?; preprocessor.enable_annotations(); let mut annotations = AnnotationTree::default(); { - let indent = dm::indents::IndentProcessor::new(&self.context, &mut preprocessor); + let indent = + dm::indents::IndentProcessor::new(&self.context, &mut preprocessor); let parser = dm::parser::Parser::new(&self.context, indent); parser.parse_annotations_only(&mut annotations); } annotations.merge(preprocessor.take_annotations().unwrap()); - v.insert((real_file_id, file_id, Rc::new(annotations))).clone() + v.insert((real_file_id, file_id, Rc::new(annotations))) + .clone() }, None => { // single-file mode let filename = url.to_string(); - let contents = self.docs.get_contents(url).map_err(invalid_request)?.into_owned(); - let mut pp = dm::preprocessor::Preprocessor::from_buffer(&self.context, filename.clone().into(), contents); - let file_id = self.context.get_file(filename.as_ref()).expect("file didn't exist?"); + let contents = self + .docs + .get_contents(url) + .map_err(invalid_request)? + .into_owned(); + let mut pp = dm::preprocessor::Preprocessor::from_buffer( + &self.context, + filename.clone().into(), + contents, + ); + let file_id = self + .context + .get_file(filename.as_ref()) + .expect("file didn't exist?"); // Clear old errors for this file. Hacky, but it will work for now. - self.context.errors_mut().retain(|error| error.location().file != file_id); + self.context + .errors_mut() + .retain(|error| error.location().file != file_id); pp.enable_annotations(); let mut annotations = AnnotationTree::default(); @@ -629,21 +723,22 @@ impl<'a> Engine<'a> { continue; } - let related_information = if !self.client_caps.related_info || error.notes().is_empty() { - None - } else { - let mut notes = Vec::with_capacity(error.notes().len()); - for note in error.notes().iter() { - notes.push(lsp_types::DiagnosticRelatedInformation { - location: lsp_types::Location { - uri: url.to_owned(), - range: location_to_range(note.location()), - }, - message: note.description().to_owned(), - }); - } - Some(notes) - }; + let related_information = + if !self.client_caps.related_info || error.notes().is_empty() { + None + } else { + let mut notes = Vec::with_capacity(error.notes().len()); + for note in error.notes().iter() { + notes.push(lsp_types::DiagnosticRelatedInformation { + location: lsp_types::Location { + uri: url.to_owned(), + range: location_to_range(note.location()), + }, + message: note.description().to_owned(), + }); + } + Some(notes) + }; let diag = lsp_types::Diagnostic { message: error.description().to_owned(), severity: Some(convert_severity(error.severity())), @@ -651,7 +746,7 @@ impl<'a> Engine<'a> { source: component_to_source(error.component()), code: convert_errorcode(error.errortype()), related_information, - .. Default::default() + ..Default::default() }; diagnostics.push(diag); @@ -660,10 +755,10 @@ impl<'a> Engine<'a> { for note in error.notes().iter() { let diag = lsp_types::Diagnostic { message: note.description().to_owned(), - severity: Some(lsp_types::DiagnosticSeverity::Information), + severity: Some(lsp_types::DiagnosticSeverity::INFORMATION), range: location_to_range(note.location()), source: component_to_source(error.component()), - .. Default::default() + ..Default::default() }; diagnostics.push(diag); } @@ -679,12 +774,15 @@ impl<'a> Engine<'a> { ); (file_id, file_id, Rc::new(annotations)) - } - } + }, + }, }) } - fn find_type_context<'b, I, Ign>(&self, iter: &I) -> (Option, Option<(&'b str, usize)>) + fn find_type_context<'b, I, Ign>( + &self, + iter: &I, + ) -> (Option>, Option<(&'b str, usize)>) where I: Iterator + Clone, { @@ -694,13 +792,11 @@ impl<'a> Engine<'a> { // chop off proc name and 'proc/' or 'verb/' if it's there // TODO: factor this logic somewhere let mut proc_path = &proc_path[..]; - match proc_path.split_last() { - Some((name, rest)) => { - proc_name = Some((name.as_str(), *idx)); - proc_path = rest; - } - _ => {} + if let Some((name, rest)) = proc_path.split_last() { + proc_name = Some((name.as_str(), *idx)); + proc_path = rest; } + match proc_path.split_last() { Some((kwd, rest)) if kwd == "proc" || kwd == "verb" => proc_path = rest, _ => {} @@ -744,13 +840,17 @@ impl<'a> Engine<'a> { } // proc parameters - let ty = ty.unwrap_or(self.objtree.root()); + let ty = ty.unwrap_or_else(|| self.objtree.root()); if let Some((proc_name, idx)) = proc_name { if let Some(proc) = ty.get().procs.get(proc_name) { if let Some(value) = proc.value.get(idx) { for param in value.parameters.iter() { if param.name == var_name { - return UnscopedVar::Parameter { ty, proc: proc_name, param }; + return UnscopedVar::Parameter { + ty, + proc: proc_name, + param, + }; } } } @@ -769,14 +869,15 @@ impl<'a> Engine<'a> { } fn find_scoped_type<'b, I>(&'b self, iter: &I, priors: &[String]) -> Option> - where I: Iterator + Clone + where + I: Iterator + Clone, { let (mut next, proc_name) = self.find_type_context(iter); // find the first; check the global scope, parameters, and "src" let mut priors = priors.iter(); let first = match priors.next() { Some(i) => i, - None => return next, // empty priors acts like unscoped + None => return next, // empty priors acts like unscoped }; if first == "args" { next = self.objtree.find("/list"); @@ -786,14 +887,20 @@ impl<'a> Engine<'a> { // nothing } else if first == "usr" { next = self.objtree.find("/mob"); + } else if first == "caller" || first == "callee" { + next = self.objtree.find("/callee"); } else { next = match self.find_unscoped_var(iter, next, proc_name, first) { - UnscopedVar::Parameter { param, .. } => self.objtree.type_by_path(param.var_type.type_path.iter()), + UnscopedVar::Parameter { param, .. } => { + self.objtree.type_by_path(param.var_type.type_path.iter()) + }, UnscopedVar::Variable { ty, .. } => match ty.get_var_declaration(first) { Some(decl) => self.objtree.type_by_path(decl.var_type.type_path.iter()), None => None, }, - UnscopedVar::Local { var_type, .. } => self.objtree.type_by_path(var_type.type_path.iter()), + UnscopedVar::Local { var_type, .. } => { + self.objtree.type_by_path(var_type.type_path.iter()) + }, UnscopedVar::None => None, }; } @@ -811,11 +918,15 @@ impl<'a> Engine<'a> { next } - fn symbol_id_at(&mut self, text_document_position: lsp_types::TextDocumentPositionParams) -> Result, jsonrpc::Error> { - let (_, file_id, annotations) = self.get_annotations(&text_document_position.text_document.uri)?; + fn symbol_id_at( + &mut self, + text_document_position: lsp_types::TextDocumentPositionParams, + ) -> Result, jsonrpc::Error> { + let (_, file_id, annotations) = + self.get_annotations(&text_document_position.text_document.uri)?; let location = dm::Location { file: file_id, - line: text_document_position.position.line as u32 + 1, + line: text_document_position.position.line + 1, column: text_document_position.position.character as u16 + 1, }; @@ -823,121 +934,126 @@ impl<'a> Engine<'a> { let iter = annotations.get_location(location); match_annotation! { iter; - Annotation::Variable(path) => { - let mut current = self.objtree.root(); - let (var_name, most) = path.split_last().unwrap(); - for part in most { - if part == "var" { break } - if let Some(child) = current.child(part) { - current = child; - } else { - break; - } - } - - if let Some(decl) = current.get_var_declaration(var_name) { - symbol_id = Some(decl.id); - } - }, - Annotation::ProcHeader(parts, _) => { - let mut current = self.objtree.root(); - let (proc_name, most) = parts.split_last().unwrap(); - for part in most { - if part == "proc" || part == "verb" { break } - if let Some(child) = current.child(part) { - current = child; - } else { - break; - } - } - - if let Some(decl) = current.get_proc_declaration(proc_name) { - symbol_id = Some(decl.id); - } - }, - Annotation::TreePath(absolute, parts) => { - if let Some(ty) = self.objtree.type_by_path(completion::combine_tree_path(&iter, *absolute, parts)) { - symbol_id = Some(ty.id); - } - }, - Annotation::TypePath(parts) => { - match self.follow_type_path(&iter, parts) { - // '/datum/proc/foo' - Some(completion::TypePathResult { ty, decl: _, proc: Some((proc_name, _)) }) => { - if let Some(decl) = ty.get_proc_declaration(proc_name) { - symbol_id = Some(decl.id); + Annotation::Variable(path) => { + let mut current = self.objtree.root(); + let (var_name, most) = path.split_last().unwrap(); + for part in most { + if part == "var" { break } + if let Some(child) = current.child(part) { + current = child; + } else { + break; } - }, - // 'datum/bar' - Some(completion::TypePathResult { ty, decl: None, proc: None }) => { + } + + if let Some(decl) = current.get_var_declaration(var_name) { + symbol_id = Some(decl.id); + } + }, + Annotation::ProcHeader(parts, _) => { + let mut current = self.objtree.root(); + let (proc_name, most) = parts.split_last().unwrap(); + for part in most { + if part == "proc" || part == "verb" { break } + if let Some(child) = current.child(part) { + current = child; + } else { + break; + } + } + + if let Some(decl) = current.get_proc_declaration(proc_name) { + symbol_id = Some(decl.id); + } + }, + Annotation::TreePath(absolute, parts) => { + if let Some(ty) = self.objtree.type_by_path(completion::combine_tree_path(&iter, *absolute, parts)) { symbol_id = Some(ty.id); - }, - _ => {} - } - }, - Annotation::UnscopedCall(proc_name) => { - let (ty, _) = self.find_type_context(&iter); - let mut next = ty.or(Some(self.objtree.root())); - while let Some(ty) = next { - if let Some(proc) = ty.procs.get(proc_name) { - if let Some(ref decl) = proc.declaration { - symbol_id = Some(decl.id); - break; - } } - next = ty.parent_type(); - } - }, - Annotation::UnscopedVar(var_name) => { - let (ty, proc_name) = self.find_type_context(&iter); - match self.find_unscoped_var(&iter, ty, proc_name, var_name) { - UnscopedVar::Parameter { .. } => { - // TODO - }, - UnscopedVar::Variable { ty, .. } => { - if let Some(decl) = ty.get_var_declaration(var_name) { - symbol_id = Some(decl.id); - } - }, - UnscopedVar::Local { .. } => { - // TODO - }, - UnscopedVar::None => {} - } - }, - Annotation::ScopedCall(priors, proc_name) => { - let mut next = self.find_scoped_type(&iter, priors); - while let Some(ty) = next { - if let Some(proc) = ty.procs.get(proc_name) { - if let Some(ref decl) = proc.declaration { - symbol_id = Some(decl.id); - break; - } + }, + Annotation::TypePath(parts) => { + match self.follow_type_path(&iter, parts) { + // '/datum/proc/foo' + Some(completion::TypePathResult { ty, decl: _, proc: Some((proc_name, _)) }) => { + if let Some(decl) = ty.get_proc_declaration(proc_name) { + symbol_id = Some(decl.id); + } + }, + // 'datum/bar' + Some(completion::TypePathResult { ty, decl: None, proc: None }) => { + symbol_id = Some(ty.id); + }, + _ => {} } - next = ty.parent_type_without_root(); - } - }, - Annotation::ScopedVar(priors, var_name) => { - let mut next = self.find_scoped_type(&iter, priors); - while let Some(ty) = next { - if let Some(var) = ty.vars.get(var_name) { - if let Some(ref decl) = var.declaration { - symbol_id = Some(decl.id); - break; + }, + Annotation::UnscopedCall(proc_name) => { + let (ty, _) = self.find_type_context(&iter); + let mut next = ty.or_else(|| Some(self.objtree.root())); + while let Some(ty) = next { + if let Some(proc) = ty.procs.get(proc_name) { + if let Some(ref decl) = proc.declaration { + symbol_id = Some(decl.id); + break; + } } + next = ty.parent_type(); } - next = ty.parent_type_without_root(); - } - }, - // TODO: macros + }, + Annotation::UnscopedVar(var_name) => { + let (ty, proc_name) = self.find_type_context(&iter); + match self.find_unscoped_var(&iter, ty, proc_name, var_name) { + UnscopedVar::Parameter { .. } => { + // TODO + }, + UnscopedVar::Variable { ty, .. } => { + if let Some(decl) = ty.get_var_declaration(var_name) { + symbol_id = Some(decl.id); + } + }, + UnscopedVar::Local { .. } => { + // TODO + }, + UnscopedVar::None => {} + } + }, + Annotation::ScopedCall(priors, proc_name) => { + let mut next = self.find_scoped_type(&iter, priors); + while let Some(ty) = next { + if let Some(proc) = ty.procs.get(proc_name) { + if let Some(ref decl) = proc.declaration { + symbol_id = Some(decl.id); + break; + } + } + next = ty.parent_type_without_root(); + } + }, + Annotation::ScopedVar(priors, var_name) => { + let mut next = self.find_scoped_type(&iter, priors); + while let Some(ty) = next { + if let Some(var) = ty.vars.get(var_name) { + if let Some(ref decl) = var.declaration { + symbol_id = Some(decl.id); + break; + } + } + next = ty.parent_type_without_root(); + } + }, + // TODO: macros } Ok(symbol_id) } - fn construct_proc_hover(&self, proc_name: &str, mut provided_tok: Option, scoped: bool) -> Result, jsonrpc::Error> { + fn construct_proc_hover( + &self, + proc_name: &str, + mut provided_tok: Option, + scoped: bool, + ) -> Result, jsonrpc::Error> { let mut results = Vec::new(); - let mut proclink = String::new(); + let mut proclink = String::new(); let mut defstring = String::new(); let mut docstring: Option = None; while let Some(ty) = provided_tok { @@ -947,8 +1063,12 @@ impl<'a> Engine<'a> { // Because we need to find our declaration to get the declaration type, we partially // form the markdown text to be used once the proc's declaration is reached if defstring.is_empty() { - proclink = format!("[{}]({})", ty.pretty_path(), self.location_link(proc_value.location)?); - let mut message = format!("{}(", proc_name); + proclink = format!( + "[{}]({})", + ty.pretty_path(), + self.location_link(proc_value.location)? + ); + let mut message = format!("{proc_name}("); let mut first = true; for each in proc_value.parameters.iter() { use std::fmt::Write; @@ -957,14 +1077,19 @@ impl<'a> Engine<'a> { } else { message.push_str(", "); } - let _ = write!(message, "{}", each); + let _ = write!(message, "{each}"); } - message.push_str(")"); - defstring = message.clone(); + message.push(')'); + defstring.clone_from(&message); } if let Some(ref decl) = proc.declaration { - results.push(format!("{}\n```dm\n{}/{}\n```", proclink, decl.kind.name(), defstring)); + results.push(format!( + "{}\n```dm\n{}/{}\n```", + proclink, + decl.kind.name(), + defstring + )); } if !proc_value.docs.is_empty() { @@ -986,7 +1111,12 @@ impl<'a> Engine<'a> { Ok(results) } - fn construct_var_hover(&self, var_name: &str, mut provided_tok: Option, scoped: bool) -> Result, jsonrpc::Error> { + fn construct_var_hover( + &self, + var_name: &str, + mut provided_tok: Option, + scoped: bool, + ) -> Result, jsonrpc::Error> { let mut results = Vec::new(); let mut infos = String::new(); let mut docstring: Option = None; @@ -994,10 +1124,19 @@ impl<'a> Engine<'a> { if let Some(var) = ty.vars.get(var_name) { if let Some(ref decl) = var.declaration { // First get the path of the type containing the declaration - infos.push_str(format!("[{}]({})\n", ty.pretty_path(), self.location_link(var.value.location)?).as_str()); + infos.push_str( + format!( + "[{}]({})\n", + ty.pretty_path(), + self.location_link(var.value.location)? + ) + .as_str(), + ); // Next toss on the declaration itself - infos.push_str(format!("```dm\nvar/{}{}\n```", decl.var_type, var_name).as_str()); + infos.push_str( + format!("```dm\nvar/{}{}\n```", decl.var_type, var_name).as_str(), + ); } if !var.value.docs.is_empty() { docstring = Some(var.value.docs.text()); @@ -1025,8 +1164,11 @@ impl<'a> Engine<'a> { fn handle_input(&mut self, message: &str) { let mut outputs: Vec = match serde_json::from_str(message) { - Ok(Request::Single(call)) => self.handle_call(call).into_iter().collect(), - Ok(Request::Batch(calls)) => calls.into_iter().flat_map(|call| self.handle_call(call)).collect(), + Ok(jsonrpc::Request::Single(call)) => self.handle_call(call).into_iter().collect(), + Ok(jsonrpc::Request::Batch(calls)) => calls + .into_iter() + .flat_map(|call| self.handle_call(call)) + .collect(), Err(decode_error) => vec![Output::Failure(jsonrpc::Failure { jsonrpc: VERSION, error: jsonrpc::Error { @@ -1039,7 +1181,7 @@ impl<'a> Engine<'a> { }; let response = match outputs.len() { - 0 => return, // wait for another input + 0 => return, // wait for another input 1 => Response::Single(outputs.remove(0)), _ => Response::Batch(outputs), }; @@ -1052,26 +1194,33 @@ impl<'a> Engine<'a> { Call::Invalid { id } => Some(Output::invalid_request(id, VERSION)), Call::MethodCall(method_call) => { let id = method_call.id.clone(); - Some(Output::from(self.handle_method_call(method_call), id, VERSION)) + Some(Output::from( + self.handle_method_call(method_call), + id, + VERSION, + )) }, Call::Notification(notification) => { if let Err(e) = self.handle_notification(notification) { - self.show_message(MessageType::Error, e.message); + self.show_message(MessageType::ERROR, e.message); } None }, } } - fn handle_method_call(&mut self, call: jsonrpc::MethodCall) -> Result { + fn handle_method_call( + &mut self, + call: jsonrpc::MethodCall, + ) -> Result { // "If the server receives a request... before the initialize request... // the response should be an error with code: -32002" - if call.method != ::METHOD && self.status != InitStatus::Running { + if call.method != Initialize::METHOD && self.status != InitStatus::Running { return Err(jsonrpc::Error { code: jsonrpc::ErrorCode::from(-32002), message: "method call before initialize or after shutdown".to_owned(), data: None, - }) + }); } let params_value = params_to_value(call.params); @@ -1087,13 +1236,22 @@ impl<'a> Engine<'a> { } } - fn handle_notification(&mut self, notification: jsonrpc::Notification) -> Result<(), jsonrpc::Error> { + fn handle_notification( + &mut self, + notification: jsonrpc::Notification, + ) -> Result<(), jsonrpc::Error> { // "Notifications should be dropped, except for the exit notification" - if notification.method == ::METHOD { - self.exit(if self.status == InitStatus::ShuttingDown { 0 } else { 1 }); + if notification.method + == ::METHOD + { + self.exit(if self.status == InitStatus::ShuttingDown { + 0 + } else { + 1 + }); } if self.status != InitStatus::Running { - return Ok(()) + return Ok(()); } let params_value = params_to_value(notification.params); @@ -1113,12 +1271,54 @@ impl<'a> Engine<'a> { } } -handle_method_call! { +// ---------------------------------------------------------------------------- +// Request handlers + +type P = ::Params; +type R = Result<::Result, jsonrpc::Error>; + +macro_rules! handle_method_call_table { + ($($what:ident;)*) => { + fn handle_method_call_table(method: &str) -> Option Result> { + match method { + $($what::METHOD => { + Some(|this, params_value| { + let params: <$what as Request>::Params = serde_json::from_value(params_value).map_err(invalid_request)?; + let result: <$what as Request>::Result = this.$what(params)?; + Ok(serde_json::to_value(result).expect("encode problem")) + }) + },)* + _ => None + } + } + } +} + +#[allow(non_snake_case)] +impl Engine { + handle_method_call_table! { + Initialize; + Shutdown; + WorkspaceSymbol; + HoverRequest; + GotoDefinition; + GotoTypeDefinition; + References; + GotoImplementation; + Completion; + SignatureHelpRequest; + DocumentSymbolRequest; + DocumentColor; + ColorPresentationRequest; + DocumentLinkRequest; + StartDebugger; + } + // ------------------------------------------------------------------------ // basic setup - on Initialize(&mut self, init) { + fn Initialize(&mut self, init: P) -> R { if self.status != InitStatus::Starting { - return Err(invalid_request("")) + return Err(invalid_request("")); } self.status = InitStatus::Running; @@ -1130,7 +1330,7 @@ handle_method_call! { let path = format!("{}/", url.path()); url.set_path(&path); } - eprintln!("workspace root: {}", url); + eprintln!("workspace root: {url}"); if let Ok(root_path) = url_to_path(&url) { let config_path = root_path.join("SpacemanDMM.toml"); @@ -1150,26 +1350,29 @@ handle_method_call! { if let (Some(start), Some(end)) = (debug.find('{'), debug.rfind('}')) { eprintln!("client capabilities: {}", &debug[start + 2..end - 1]); } else { - eprintln!("client capabilities: {}", debug); + eprintln!("client capabilities: {debug}"); } eprintln!(); - InitializeResult { + Ok(InitializeResult { capabilities: ServerCapabilities { - definition_provider: Some(true), - workspace_symbol_provider: Some(true), + definition_provider: Some(OneOf::Left(true)), + workspace_symbol_provider: Some(OneOf::Left(true)), hover_provider: Some(HoverProviderCapability::Simple(true)), - document_symbol_provider: Some(true), - references_provider: Some(true), + document_symbol_provider: Some(OneOf::Left(true)), + references_provider: Some(OneOf::Left(true)), implementation_provider: Some(ImplementationProviderCapability::Simple(true)), type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), - text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions { - open_close: Some(true), - change: Some(TextDocumentSyncKind::Incremental), - .. Default::default() - })), + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + ..Default::default() + }, + )), completion_provider: Some(CompletionOptions { trigger_characters: Some(vec![".".to_owned(), ":".to_owned(), "/".to_owned()]), + all_commit_characters: None, resolve_provider: None, work_done_progress_options: Default::default(), }), @@ -1183,23 +1386,24 @@ handle_method_call! { work_done_progress_options: Default::default(), }), color_provider: Some(ColorProviderCapability::Simple(true)), - .. Default::default() + ..Default::default() }, server_info: Some(ServerInfo { name: "dm-langserver".to_owned(), version: Some(env!("CARGO_PKG_VERSION").to_owned()), }), - } + }) } - on Shutdown(&mut self, ()) { + fn Shutdown(&mut self, (): P) -> R { self.status = InitStatus::ShuttingDown; + Ok(()) } // ------------------------------------------------------------------------ // actual stuff provision - #[allow(deprecated)] // DocumentSymbol::deprecated is... deprecated. But we need to provide a `None` anyways. - on WorkspaceSymbol(&mut self, params) { + #[allow(deprecated)] // DocumentSymbol::deprecated is... deprecated. But we need to provide a `None` anyways. + fn WorkspaceSymbol(&mut self, params: P) -> R { let query = symbol_search::Query::parse(¶ms.query); let query = match query { @@ -1209,13 +1413,18 @@ handle_method_call! { let mut results = Vec::new(); if let Some(ref defines) = self.defines { - for (range, &(ref name, ref define)) in defines.iter() { + for (range, (name, define)) in defines.iter() { if query.matches_define(name) { results.push(SymbolInformation { name: name.to_owned(), - kind: SymbolKind::Constant, - location: self.convert_location(range.start, define.docs(), &["/DM/preprocessor/", name])?, + kind: SymbolKind::CONSTANT, + location: self.convert_location( + range.start, + define.docs(), + &["/DM/preprocessor/", name], + )?, container_name: None, + tags: None, deprecated: None, }); } @@ -1226,9 +1435,10 @@ handle_method_call! { if query.matches_type(ty.name(), &ty.path) && !ty.is_root() { results.push(SymbolInformation { name: ty.name().to_owned(), - kind: SymbolKind::Class, + kind: SymbolKind::CLASS, location: self.convert_location(ty.location, &ty.docs, &[&ty.path])?, container_name: Some(ty.parent_path_str().to_owned()), + tags: None, deprecated: None, }); } @@ -1238,12 +1448,17 @@ handle_method_call! { } for (var_name, tv) in ty.vars.iter() { if let Some(decl) = tv.declaration.as_ref() { - if query.matches_var(&var_name) { + if query.matches_var(var_name) { results.push(SymbolInformation { name: var_name.clone(), - kind: SymbolKind::Field, - location: self.convert_location(decl.location, &tv.value.docs, &[&ty.path, "/var/", var_name])?, + kind: SymbolKind::FIELD, + location: self.convert_location( + decl.location, + &tv.value.docs, + &[&ty.path, "/var/", var_name], + )?, container_name: Some(ty.path.clone()), + tags: None, deprecated: None, }); } @@ -1252,33 +1467,38 @@ handle_method_call! { for (proc_name, pv) in ty.procs.iter() { if let Some(decl) = pv.declaration.as_ref() { - if query.matches_proc(&proc_name, decl.kind) { + if query.matches_proc(proc_name, decl.kind) { results.push(SymbolInformation { name: proc_name.clone(), kind: if ty.is_root() { - SymbolKind::Function + SymbolKind::FUNCTION } else if is_constructor_name(proc_name.as_str()) { - SymbolKind::Constructor + SymbolKind::CONSTRUCTOR } else { - SymbolKind::Method + SymbolKind::METHOD }, - location: self.convert_location(decl.location, &pv.main_value().docs, &[&ty.path, "/proc/", proc_name])?, + location: self.convert_location( + decl.location, + &pv.main_value().docs, + &[&ty.path, "/proc/", proc_name], + )?, container_name: Some(ty.path.clone()), + tags: None, deprecated: None, }); } } } } - Some(results) + Ok(Some(results)) } - on HoverRequest(&mut self, params) { + fn HoverRequest(&mut self, params: P) -> R { let tdp = params.text_document_position_params; let (_, file_id, annotations) = self.get_annotations(&tdp.text_document.uri)?; let location = dm::Location { file: file_id, - line: tdp.position.line as u32 + 1, + line: tdp.position.line + 1, column: tdp.position.character as u16 + 1, }; let symbol_id = self.symbol_id_at(tdp)?; @@ -1286,8 +1506,9 @@ handle_method_call! { let iter = annotations.get_location(location); for (_range, annotation) in iter.clone() { - #[cfg(debug_assertions)] { - results.push(format!("{:?}", annotation)); + #[cfg(debug_assertions)] + { + results.push(format!("{annotation:?}")); } match annotation { Annotation::Variable(path) if !path.is_empty() => { @@ -1295,7 +1516,9 @@ handle_method_call! { let mut current = objtree.root(); let (last, most) = path.split_last().unwrap(); for part in most { - if part == "var" { break } + if part == "var" { + break; + } if let Some(child) = current.child(part) { current = child; } else { @@ -1309,13 +1532,21 @@ handle_method_call! { while let Some(current) = next { if let Some(var) = current.vars.get(last) { let constant = if let Some(ref constant) = var.value.constant { - format!("\n```dm\n= {}\n```", constant) + format!("\n```dm\n= {constant}\n```") } else { String::new() }; - infos.push_front(format!("[{}]({}){}", current.pretty_path(), self.location_link(var.value.location)?, constant)); + infos.push_front(format!( + "[{}]({}){}", + current.pretty_path(), + self.location_link(var.value.location)?, + constant + )); if let Some(ref decl) = var.declaration { - infos.push_front(format!("```dm\nvar/{}{}\n```", decl.var_type, last)); + infos.push_front(format!( + "```dm\nvar/{}{}\n```", + decl.var_type, last + )); } if !var.value.docs.is_empty() { docstring = Some(var.value.docs.text()); @@ -1329,13 +1560,15 @@ handle_method_call! { if let Some(ds) = docstring { results.push(ds); } - } + }, Annotation::ProcHeader(path, _idx) if !path.is_empty() => { let objtree = &self.objtree; let mut current = objtree.root(); let (last, most) = path.split_last().unwrap(); for part in most { - if part == "proc" || part == "verb" { break } + if part == "proc" || part == "verb" { + break; + } if let Some(child) = current.child(part) { current = child; } else { @@ -1351,7 +1584,12 @@ handle_method_call! { while let Some(current) = next { if let Some(proc) = current.procs.get(last) { let proc_value = proc.main_value(); - let mut message = format!("[{}]({}) \n```dm\n{}(", current.pretty_path(), self.location_link(proc_value.location)?, last); + let mut message = format!( + "[{}]({}) \n```dm\n{}(", + current.pretty_path(), + self.location_link(proc_value.location)?, + last + ); let mut first = true; for each in proc_value.parameters.iter() { use std::fmt::Write; @@ -1360,12 +1598,16 @@ handle_method_call! { } else { message.push_str(", "); } - let _ = write!(message, "{}", each); + let _ = write!(message, "{each}"); } message.push_str(")\n```"); infos.push_front(message); if let Some(ref decl) = proc.declaration { - infos.push_front(format!("```dm\n{}/{}\n```", decl.kind.name(), last)); + infos.push_front(format!( + "```dm\n{}/{}\n```", + decl.kind.name(), + last + )); } if !proc_value.docs.is_empty() { @@ -1380,147 +1622,78 @@ handle_method_call! { if let Some(ds) = docstring { results.push(ds); } - } + }, Annotation::UnscopedVar(var_name) if symbol_id.is_some() => { let (ty, proc_name) = self.find_type_context(&iter); - match self.find_unscoped_var(&iter, ty, proc_name, var_name) { - UnscopedVar::Variable { ty, .. } => { - if let Some(_decl) = ty.get_var_declaration(var_name) { - results.append(&mut self.construct_var_hover(var_name, Some(ty), false)?); - } - }, - _ => {} + if let UnscopedVar::Variable { ty, .. } = + self.find_unscoped_var(&iter, ty, proc_name, var_name) + { + if let Some(_decl) = ty.get_var_declaration(var_name) { + results.append(&mut self.construct_var_hover( + var_name, + Some(ty), + false, + )?); + } } - } + }, Annotation::UnscopedCall(proc_name) if symbol_id.is_some() => { let (ty, _) = self.find_type_context(&iter); - let next = ty.or(Some(self.objtree.root())); + let next = ty.or_else(|| Some(self.objtree.root())); results.append(&mut self.construct_proc_hover(proc_name, next, false)?); - } + }, Annotation::ScopedCall(priors, proc_name) if symbol_id.is_some() => { let next = self.find_scoped_type(&iter, priors); results.append(&mut self.construct_proc_hover(proc_name, next, true)?); - } + }, Annotation::ScopedVar(priors, var_name) if symbol_id.is_some() => { let next = self.find_scoped_type(&iter, priors); results.append(&mut self.construct_var_hover(var_name, next, true)?); - } - _ => {} + }, + Annotation::MacroUse { docs: Some(dc), .. } if !dc.is_empty() => { + results.push(dc.text()); + }, + _ => {}, } } if results.is_empty() { - None + Ok(None) } else { - Some(Hover { + Ok(Some(Hover { range: None, - contents: HoverContents::Array(results.into_iter().map(MarkedString::String).collect()), - }) + contents: HoverContents::Array( + results.into_iter().map(MarkedString::String).collect(), + ), + })) } } - on GotoDefinition(&mut self, params) { + fn GotoDefinition(&mut self, params: P) -> R { let tdp = params.text_document_position_params; let (real_file_id, file_id, annotations) = self.get_annotations(&tdp.text_document.uri)?; let location = dm::Location { file: file_id, - line: tdp.position.line as u32 + 1, + line: tdp.position.line + 1, column: tdp.position.character as u16 + 1, }; let mut results = Vec::new(); let iter = annotations.get_location(location); match_annotation! { iter; - Annotation::TreePath(absolute, parts) => { - let full_path: Vec<&str> = completion::combine_tree_path(&iter, *absolute, parts).collect(); + Annotation::TreePath(absolute, parts) => { + let full_path: Vec<&str> = completion::combine_tree_path(&iter, *absolute, parts).collect(); - if let Some(ty) = self.objtree.type_by_path(full_path.iter().cloned()) { - results.push(self.convert_location(ty.location, &ty.docs, &[&ty.path])?); - } else if let Some((&proc_name, prefix)) = full_path.split_last() { - // If it's not a type, try to find the proc equivalent. Start - // at the parent type so that this is a decent shortcut for - // going to the parent proc. - // TODO: only do this if we're in a ProcHeader. - let mut next = self.objtree.type_by_path(prefix); - if let Some(ty) = next { - next = ty.parent_type(); - } - while let Some(ty) = next { - if let Some(proc) = ty.procs.get(proc_name) { - results.push(self.convert_location(proc.main_value().location, &proc.main_value().docs, &[&ty.path, "/proc/", proc_name])?); - break; - } - next = ty.parent_type(); - } - } - }, - Annotation::TypePath(parts) => { - match self.follow_type_path(&iter, parts) { - // '/datum/proc/foo' - Some(completion::TypePathResult { ty, decl: _, proc: Some((proc_name, proc)) }) => { - results.push(self.convert_location(proc.location, &proc.docs, &[&ty.path, "/proc/", proc_name])?); - }, - // 'datum/bar' - Some(completion::TypePathResult { ty, decl: None, proc: None }) => { + if let Some(ty) = self.objtree.type_by_path(full_path.iter().cloned()) { results.push(self.convert_location(ty.location, &ty.docs, &[&ty.path])?); - }, - _ => {} - } - }, - Annotation::UnscopedCall(proc_name) => { - let (ty, _) = self.find_type_context(&iter); - let mut next = ty.or(Some(self.objtree.root())); - while let Some(ty) = next { - if let Some(proc) = ty.procs.get(proc_name) { - results.push(self.convert_location(proc.main_value().location, &proc.main_value().docs, &[&ty.path, "/proc/", proc_name])?); - break; - } - next = ty.parent_type(); - } - }, - Annotation::UnscopedVar(var_name) => { - let (ty, proc_name) = self.find_type_context(&iter); - match self.find_unscoped_var(&iter, ty, proc_name, var_name) { - UnscopedVar::Parameter { ty, proc, param } => { - results.push(self.convert_location(param.location, &Default::default(), &[&ty.path, "/proc/", proc])?); - }, - UnscopedVar::Variable { ty, var } => { - results.push(self.convert_location(var.value.location, &var.value.docs, &[&ty.path, "/var/", var_name])?); - }, - UnscopedVar::Local { loc, .. } => { - results.push(self.convert_location(dm::Location { file: real_file_id, ..loc }, &Default::default(), &[])?); - }, - UnscopedVar::None => {} - } - }, - Annotation::ScopedCall(priors, proc_name) => { - let mut next = self.find_scoped_type(&iter, priors); - while let Some(ty) = next { - if let Some(proc) = ty.procs.get(proc_name) { - results.push(self.convert_location(proc.main_value().location, &proc.main_value().docs, &[&ty.path, "/proc/", proc_name])?); - break; - } - next = ty.parent_type_without_root(); - } - }, - Annotation::ScopedVar(priors, var_name) => { - let mut next = self.find_scoped_type(&iter, priors); - while let Some(ty) = next { - if let Some(var) = ty.vars.get(var_name) { - results.push(self.convert_location(var.value.location, &var.value.docs, &[&ty.path, "/var/", var_name])?); - break; - } - next = ty.parent_type_without_root(); - } - }, - Annotation::ParentCall => { - if let (Some(ty), Some((proc_name, idx))) = self.find_type_context(&iter) { - // TODO: idx is always 0 unless there are multiple overrides in - // the same .dm file, due to annotations operating against a - // dummy ObjectTree which does not contain any definitions from - // other files. - if idx == 0 { - // first proc on the type, go to the REAL parent - let mut next = ty.parent_type(); + } else if let Some((&proc_name, prefix)) = full_path.split_last() { + // If it's not a type, try to find the proc equivalent. Start + // at the parent type so that this is a decent shortcut for + // going to the parent proc. + // TODO: only do this if we're in a ProcHeader. + let mut next = self.objtree.type_by_path(prefix); + if let Some(ty) = next { + next = ty.parent_type(); + } while let Some(ty) = next { if let Some(proc) = ty.procs.get(proc_name) { results.push(self.convert_location(proc.main_value().location, &proc.main_value().docs, &[&ty.path, "/proc/", proc_name])?); @@ -1528,34 +1701,110 @@ handle_method_call! { } next = ty.parent_type(); } - } else if let Some(proc) = ty.procs.get(proc_name) { - // override, go to the previous version of the proc - if let Some(parent) = proc.value.get(idx - 1) { - results.push(self.convert_location(parent.location, &parent.docs, &[&ty.path, "/proc/", proc_name])?); + } + }, + Annotation::TypePath(parts) => { + match self.follow_type_path(&iter, parts) { + // '/datum/proc/foo' + Some(completion::TypePathResult { ty, decl: _, proc: Some((proc_name, proc)) }) => { + results.push(self.convert_location(proc.location, &proc.docs, &[&ty.path, "/proc/", proc_name])?); + }, + // 'datum/bar' + Some(completion::TypePathResult { ty, decl: None, proc: None }) => { + results.push(self.convert_location(ty.location, &ty.docs, &[&ty.path])?); + }, + _ => {} + } + }, + Annotation::UnscopedCall(proc_name) => { + let (ty, _) = self.find_type_context(&iter); + let mut next = ty.or_else(|| Some(self.objtree.root())); + while let Some(ty) = next { + if let Some(proc) = ty.procs.get(proc_name) { + results.push(self.convert_location(proc.main_value().location, &proc.main_value().docs, &[&ty.path, "/proc/", proc_name])?); + break; + } + next = ty.parent_type(); + } + }, + Annotation::UnscopedVar(var_name) => { + let (ty, proc_name) = self.find_type_context(&iter); + match self.find_unscoped_var(&iter, ty, proc_name, var_name) { + UnscopedVar::Parameter { ty, proc, param } => { + results.push(self.convert_location(param.location, &Default::default(), &[&ty.path, "/proc/", proc])?); + }, + UnscopedVar::Variable { ty, var } => { + results.push(self.convert_location(var.value.location, &var.value.docs, &[&ty.path, "/var/", var_name])?); + }, + UnscopedVar::Local { loc, .. } => { + results.push(self.convert_location(dm::Location { file: real_file_id, ..loc }, &Default::default(), &[])?); + }, + UnscopedVar::None => {} + } + }, + Annotation::ScopedCall(priors, proc_name) => { + let mut next = self.find_scoped_type(&iter, priors); + while let Some(ty) = next { + if let Some(proc) = ty.procs.get(proc_name) { + results.push(self.convert_location(proc.main_value().location, &proc.main_value().docs, &[&ty.path, "/proc/", proc_name])?); + break; + } + next = ty.parent_type_without_root(); + } + }, + Annotation::ScopedVar(priors, var_name) => { + let mut next = self.find_scoped_type(&iter, priors); + while let Some(ty) = next { + if let Some(var) = ty.vars.get(var_name) { + results.push(self.convert_location(var.value.location, &var.value.docs, &[&ty.path, "/var/", var_name])?); + break; + } + next = ty.parent_type_without_root(); + } + }, + Annotation::ParentCall => { + if let (Some(ty), Some((proc_name, idx))) = self.find_type_context(&iter) { + // TODO: idx is always 0 unless there are multiple overrides in + // the same .dm file, due to annotations operating against a + // dummy ObjectTree which does not contain any definitions from + // other files. + if idx == 0 { + // first proc on the type, go to the REAL parent + let mut next = ty.parent_type(); + while let Some(ty) = next { + if let Some(proc) = ty.procs.get(proc_name) { + results.push(self.convert_location(proc.main_value().location, &proc.main_value().docs, &[&ty.path, "/proc/", proc_name])?); + break; + } + next = ty.parent_type(); + } + } else if let Some(proc) = ty.procs.get(proc_name) { + // override, go to the previous version of the proc + if let Some(parent) = proc.value.get(idx - 1) { + results.push(self.convert_location(parent.location, &parent.docs, &[&ty.path, "/proc/", proc_name])?); + } } } - } - }, - Annotation::MacroUse(name, location) => { - // TODO: get docs for this macro - results.push(self.convert_location(*location, &Default::default(), &["/DM/preprocessor/", name])?); - }, + }, + Annotation::MacroUse { name, definition_location, .. } => { + results.push(self.convert_location(*definition_location, &Default::default(), &["/DM/preprocessor/", name])?); + }, } if results.is_empty() { - None + Ok(None) } else { - Some(GotoDefinitionResponse::Array(results)) + Ok(Some(GotoDefinitionResponse::Array(results))) } } - on GotoTypeDefinition(&mut self, params) { + fn GotoTypeDefinition(&mut self, params: P) -> R { // Like GotoDefinition, but only supports vars, then finds their types let tdp = params.text_document_position_params; let (_, file_id, annotations) = self.get_annotations(&tdp.text_document.uri)?; let location = dm::Location { file: file_id, - line: tdp.position.line as u32 + 1, + line: tdp.position.line + 1, column: tdp.position.character as u16 + 1, }; @@ -1563,48 +1812,48 @@ handle_method_call! { let iter = annotations.get_location(location); match_annotation! { iter; - Annotation::UnscopedVar(var_name) => { - let (ty, proc_name) = self.find_type_context(&iter); - match self.find_unscoped_var(&iter, ty, proc_name, var_name) { - UnscopedVar::Parameter { param, .. } => { - type_path = ¶m.var_type.type_path; - }, - UnscopedVar::Variable { ty, .. } => { - if let Some(decl) = ty.get_var_declaration(var_name) { - type_path = &decl.var_type.type_path; - } - }, - UnscopedVar::Local { var_type, .. } => { - type_path = &var_type.type_path; - }, - UnscopedVar::None => {} - } - }, - Annotation::ScopedVar(priors, var_name) => { - let mut next = self.find_scoped_type(&iter, priors); - while let Some(ty) = next { - if let Some(var) = ty.get().vars.get(var_name) { - if let Some(ref decl) = var.declaration { - type_path = &decl.var_type.type_path; - break; - } + Annotation::UnscopedVar(var_name) => { + let (ty, proc_name) = self.find_type_context(&iter); + match self.find_unscoped_var(&iter, ty, proc_name, var_name) { + UnscopedVar::Parameter { param, .. } => { + type_path = ¶m.var_type.type_path; + }, + UnscopedVar::Variable { ty, .. } => { + if let Some(decl) = ty.get_var_declaration(var_name) { + type_path = &decl.var_type.type_path; + } + }, + UnscopedVar::Local { var_type, .. } => { + type_path = &var_type.type_path; + }, + UnscopedVar::None => {} } - next = ty.parent_type_without_root(); - } - }, + }, + Annotation::ScopedVar(priors, var_name) => { + let mut next = self.find_scoped_type(&iter, priors); + while let Some(ty) = next { + if let Some(var) = ty.get().vars.get(var_name) { + if let Some(ref decl) = var.declaration { + type_path = &decl.var_type.type_path; + break; + } + } + next = ty.parent_type_without_root(); + } + }, } if type_path.is_empty() { - None + Ok(None) } else if let Some(ty) = self.objtree.type_by_path(type_path) { let ty_loc = self.convert_location(ty.location, &ty.docs, &[&ty.path])?; - Some(GotoDefinitionResponse::Scalar(ty_loc)) + Ok(Some(GotoDefinitionResponse::Scalar(ty_loc))) } else { - None + Ok(None) } } - on References(&mut self, params) { + fn References(&mut self, params: P) -> R { // Like GotoDefinition, but looks up references instead let symbol_id = self.symbol_id_at(params.text_document_position)?; @@ -1616,17 +1865,17 @@ handle_method_call! { } } if result.is_empty() { - None + Ok(None) } else { let mut output = Vec::new(); for each in result { output.push(self.convert_location(*each, &Default::default(), &[])?); } - Some(output) + Ok(Some(output)) } } - on GotoImplementation(&mut self, params) { + fn GotoImplementation(&mut self, params: P) -> R { let tdp = params.text_document_position_params; let symbol_id = self.symbol_id_at(tdp)?; @@ -1638,21 +1887,22 @@ handle_method_call! { } } if result.is_empty() { - None + Ok(None) } else { let mut output = Vec::new(); for each in result { output.push(self.convert_location(*each, &Default::default(), &[])?); } - Some(GotoDefinitionResponse::Array(output)) + Ok(Some(GotoDefinitionResponse::Array(output))) } } - on Completion(&mut self, params) { - let (_, file_id, annotations) = self.get_annotations(¶ms.text_document_position.text_document.uri)?; + fn Completion(&mut self, params: P) -> R { + let (_, file_id, annotations) = + self.get_annotations(¶ms.text_document_position.text_document.uri)?; let location = dm::Location { file: file_id, - line: params.text_document_position.position.line as u32 + 1, + line: params.text_document_position.position.line + 1, column: params.text_document_position.position.character as u16 + 1, }; let iter = annotations.get_location(location); @@ -1713,23 +1963,28 @@ handle_method_call! { // TODO: unscoped_completions calls find_type_context again self.unscoped_completions(&mut results, &iter, ""); } else { - self.tree_completions(&mut results, true, ty.unwrap_or(self.objtree.root()), ""); + self.tree_completions( + &mut results, + true, + ty.unwrap_or_else(|| self.objtree.root()), + "", + ); } } if results.is_empty() { - None + Ok(None) } else { - Some(CompletionResponse::Array(results)) + Ok(Some(CompletionResponse::Array(results))) } } - on SignatureHelpRequest(&mut self, params) { + fn SignatureHelpRequest(&mut self, params: P) -> R { let tdp = params.text_document_position_params; let (_, file_id, annotations) = self.get_annotations(&tdp.text_document.uri)?; let location = dm::Location { file: file_id, - line: tdp.position.line as u32 + 1, + line: tdp.position.line + 1, column: tdp.position.character as u16 + 1, }; let iter = annotations.get_location(location); @@ -1751,7 +2006,7 @@ handle_method_call! { let mut sep = ""; for param in proc.main_value().parameters.iter() { for each in param.var_type.type_path.iter() { - let _ = write!(label, "{}{}", sep, each); + let _ = write!(label, "{sep}{each}"); sep = "/"; } label.push_str(sep); @@ -1762,7 +2017,7 @@ handle_method_call! { if self.client_caps.label_offset_support { params.push(ParameterInformation { - label: ParameterLabel::LabelOffsets([start as u64, end as u64]), + label: ParameterLabel::LabelOffsets([start as u32, end as u32]), documentation: None, }); } else { @@ -1776,11 +2031,12 @@ handle_method_call! { result = Some(SignatureHelp { active_signature: Some(0), - active_parameter: Some(idx as i64), + active_parameter: Some(idx as u32), signatures: vec![SignatureInformation { - label: label, + label, parameters: Some(params), documentation: None, + active_parameter: None, }], }); break; @@ -1794,21 +2050,41 @@ handle_method_call! { } }} - result + Ok(result) } - #[allow(deprecated)] // DocumentSymbol::deprecated is... deprecated. But we need to provide a `None` anyways. - on DocumentSymbolRequest(&mut self, params) { - fn name_and_detail(path: &[String]) -> (String, Option) { + #[allow(deprecated)] // DocumentSymbol::deprecated is... deprecated. But we need to provide a `None` anyways. + fn DocumentSymbolRequest( + &mut self, + params: P, + ) -> R { + fn name_and_detail(path: &[String], skip_front: usize) -> (String, Option) { let (name, rest) = path.split_last().unwrap(); - (name.to_owned(), rest.last().map(ToOwned::to_owned)) + ( + name.to_owned(), + rest.get(skip_front..).and_then(|i| { + i.iter() + .rev() + .find(|x| { + dm::ast::ProcDeclKind::from_name(x).is_none() + && dm::ast::ProcFlags::from_name(x).is_none() + && dm::ast::VarTypeFlags::from_name(x).is_none() + && *x != "var" + }) + .map(ToOwned::to_owned) + }), + ) } // recursive traversal - fn find_document_symbols( - iter: &mut std::iter::Peekable, + fn find_document_symbols<'a, I>( + iter: &mut std::iter::Peekable, section_end: dm::Location, - ) -> Vec { + skip_front: usize, + ) -> Vec + where + I: Iterator, &'a Annotation)>, + { let mut result = Vec::new(); loop { @@ -1818,9 +2094,7 @@ handle_method_call! { } } - let (child_range, annotation) = if let Some(x) = iter.next() { - x - } else { + let Some((child_range, annotation)) = iter.next() else { break; }; @@ -1829,23 +2103,28 @@ handle_method_call! { let selection_range = location_to_range(start); match annotation { Annotation::TreeBlock(ref path) => { - if path.is_empty() { continue } - let (name, detail) = name_and_detail(path); + if path.is_empty() { + continue; + } + let (name, detail) = name_and_detail(path, skip_front); result.push(DocumentSymbol { name, detail, - kind: SymbolKind::Class, + kind: SymbolKind::CLASS, + tags: None, deprecated: None, range, selection_range, - children: Some(find_document_symbols(iter, end)), + children: Some(find_document_symbols(iter, end, path.len())), }); }, Annotation::Variable(ref path) => { + let (name, detail) = name_and_detail(path, skip_front); result.push(DocumentSymbol { - name: path.last().unwrap().to_owned(), - detail: None, - kind: SymbolKind::Field, + name, + detail, + kind: SymbolKind::FIELD, + tags: None, deprecated: None, range, selection_range, @@ -1853,48 +2132,51 @@ handle_method_call! { }); }, Annotation::ProcBody(ref path, _) => { - if path.is_empty() { continue } - let (name, detail) = name_and_detail(path); + if path.is_empty() { + continue; + } + let (name, detail) = name_and_detail(path, skip_front); let kind = if path.len() == 1 || (path.len() == 2 && path[0] == "proc") { - SymbolKind::Function + SymbolKind::FUNCTION } else if is_constructor_name(&name) { - SymbolKind::Constructor + SymbolKind::CONSTRUCTOR } else { - SymbolKind::Method + SymbolKind::METHOD }; result.push(DocumentSymbol { name, detail, kind, deprecated: None, + tags: None, range, selection_range, - children: Some(find_document_symbols(iter, end)), + children: Some(find_document_symbols(iter, end, 0)), }); }, Annotation::LocalVarScope(_, ref name) => { result.push(DocumentSymbol { name: name.to_owned(), detail: None, - kind: SymbolKind::Variable, + kind: SymbolKind::VARIABLE, + tags: None, deprecated: None, range, selection_range, children: None, }); }, - Annotation::MacroDefinition(ref name) => { - result.push(DocumentSymbol { - name: name.to_owned(), - detail: None, - kind: SymbolKind::Constant, - deprecated: None, - range, - selection_range, - children: None, - }) - }, - _ => {} + Annotation::MacroDefinition(ref name) => result.push(DocumentSymbol { + name: name.to_owned(), + detail: None, + kind: SymbolKind::CONSTANT, + tags: None, + deprecated: None, + range, + selection_range, + children: None, + }), + _ => {}, } } @@ -1904,17 +2186,45 @@ handle_method_call! { // root let (_, file_id, annotations) = self.get_annotations(¶ms.text_document.uri)?; if annotations.is_empty() { - None + Ok(None) } else { - let start = dm::Location { file: file_id, line: 0, column: 0 }; - let end = dm::Location { file: file_id, line: !0, column: !0 }; - let mut iter = annotations.get_range(start..end).peekable(); - Some(DocumentSymbolResponse::Nested(find_document_symbols(&mut iter, end))) + let start = dm::Location { + file: file_id, + line: 0, + column: 0, + }; + let end = dm::Location { + file: file_id, + line: !0, + column: !0, + }; + let mut vec: Vec<_> = annotations.get_range(start..end).collect(); + // Our traversal is outside-in. TreeBlock and Variable can have the + // same extent if the variable is the only thing in the block, so + // sort TreeBlocks first as well. + vec.sort_by_key(|x| { + ( + x.0.start, + std::cmp::Reverse(x.0.end), + if matches!(x.1, Annotation::TreeBlock(_)) { + 0 + } else { + 1 + }, + ) + }); + let mut iter = vec.into_iter().peekable(); + Ok(Some(DocumentSymbolResponse::Nested(find_document_symbols( + &mut iter, end, 0, + )))) } } - on DocumentColor(&mut self, params) { - let content = self.docs.get_contents(¶ms.text_document.uri).map_err(invalid_request)?; + fn DocumentColor(&mut self, params: P) -> R { + let content = self + .docs + .get_contents(¶ms.text_document.uri) + .map_err(invalid_request)?; let mut output = Vec::new(); for (start, end, [r, g, b, a]) in color::extract_colors(&content) { output.push(ColorInformation { @@ -1923,39 +2233,43 @@ handle_method_call! { end: document::offset_to_position(&content, end), }, color: Color { - red: (r as f64) / 255., - green: (g as f64) / 255., - blue: (b as f64) / 255., - alpha: (a as f64) / 255., + red: (r as f32) / 255., + green: (g as f32) / 255., + blue: (b as f32) / 255., + alpha: (a as f32) / 255., }, }); } - output + Ok(output) } - on ColorPresentationRequest(&mut self, params) { - let content = self.docs.get_contents(¶ms.text_document.uri).map_err(invalid_request)?; + fn ColorPresentationRequest( + &mut self, + params: P, + ) -> R { + let content = self + .docs + .get_contents(¶ms.text_document.uri) + .map_err(invalid_request)?; let chunk = document::get_range(&content, params.range)?; - let color_format = color::ColorFormat::parse(&chunk).unwrap_or_default(); + let color_format = color::ColorFormat::parse(chunk).unwrap_or_default(); // TODO: return compatible alternate presentations for converting // between "#..." and rgb(). - vec![ - ColorPresentation { - label: color_format.format([ - (params.color.red * 255.).round() as u8, - (params.color.green * 255.).round() as u8, - (params.color.blue * 255.).round() as u8, - (params.color.alpha * 255.).round() as u8, - ]), - .. Default::default() - }, - ] + Ok(vec![ColorPresentation { + label: color_format.format([ + (params.color.red * 255.).round() as u8, + (params.color.green * 255.).round() as u8, + (params.color.blue * 255.).round() as u8, + (params.color.alpha * 255.).round() as u8, + ]), + ..Default::default() + }]) } - on DocumentLinkRequest(&mut self, params) { + fn DocumentLinkRequest(&mut self, params: P) -> R { let (_, file_id, annotations) = self.get_annotations(¶ms.text_document.uri)?; if annotations.is_empty() { - None + Ok(None) } else { let mut results = Vec::new(); for (span, annotation) in annotations.iter() { @@ -1963,10 +2277,9 @@ handle_method_call! { continue; } match annotation { - Annotation::Include(path) | - Annotation::Resource(path) => { + Annotation::Include(path) | Annotation::Resource(path) => { let pathbuf = if path.is_relative() { - std::env::current_dir().map_err(invalid_request)?.join(&path) + std::env::current_dir().map_err(invalid_request)?.join(path) } else { path.to_owned() }; @@ -1976,18 +2289,18 @@ handle_method_call! { tooltip: None, data: None, }); - } - _ => {} + }, + _ => {}, } } - Some(results) + Ok(Some(results)) } } // ------------------------------------------------------------------------ // debugger entry point - on StartDebugger(&mut self, params) { + fn StartDebugger(&mut self, params: P) -> R { let root_dir = match self.root.as_ref() { Some(url) => url_to_path(url)?, None => Default::default(), @@ -1999,16 +2312,55 @@ handle_method_call! { extools_dll: self.extools_dll.clone(), debug_server_dll: self.debug_server_dll.clone(), }; - let (port, handle) = debugger::start_server(self.context.config().debugger.engine, params.dreamseeker_exe, db).map_err(invalid_request)?; + let (port, handle) = debugger::start_server( + self.context.config().debugger.engine, + params.dreamseeker_exe, + params.env, + db, + ) + .map_err(invalid_request)?; self.threads.push(handle); - extras::StartDebuggerResult { port } + Ok(extras::StartDebuggerResult { port }) } } -handle_notification! { +// ---------------------------------------------------------------------------- +// Notification handlers + +type N = ::Params; +type NR = Result<(), jsonrpc::Error>; + +macro_rules! handle_notification_table { + ($($what:ident;)*) => { + fn handle_notification_table(method: &str) -> Option Result<(), jsonrpc::Error>> { + match method { + $($what::METHOD => { + Some(|this, params_value| { + let params: <$what as Notification>::Params = serde_json::from_value(params_value).map_err(invalid_request)?; + this.$what(params) + }) + },)* + _ => None + } + } + } +} + +#[allow(non_snake_case)] +impl Engine { + handle_notification_table! { + Initialized; + Reparse; + Cancel; + DidOpenTextDocument; + DidCloseTextDocument; + DidChangeTextDocument; + DidChangeConfiguration; + } + // ------------------------------------------------------------------------ // basic setup - on Initialized(&mut self, _) { + fn Initialized(&mut self, _: N) -> NR { let mut environment = None; if let Some(ref root) = self.root { // TODO: support non-files here @@ -2016,7 +2368,8 @@ handle_notification! { if let Some(dme) = self.context.config().environment.as_ref() { environment = Some(root_path.join(dme)); } else { - environment = dm::detect_environment(&root_path, dm::DEFAULT_ENV).map_err(invalid_request)?; + environment = dm::detect_environment(&root_path, dm::DEFAULT_ENV) + .map_err(invalid_request)?; } } } @@ -2028,40 +2381,56 @@ handle_notification! { } else { self.show_status("single file mode"); } + Ok(()) } - on Reparse(&mut self, _p) { + fn Reparse(&mut self, _p: N) -> NR { eprintln!(); eprintln!("reparsing by request..."); self.context.errors_mut().clear(); - return self.Initialized(_p); + self.Initialized(_p) } - on Cancel(&mut self, _) { /* Not implemented, but don't log that. */ } + fn Cancel(&mut self, _: N) -> NR { + // Not implemented, but don't log that. + Ok(()) + } // ------------------------------------------------------------------------ // document content management - on DidOpenTextDocument(&mut self, params) { + fn DidOpenTextDocument(&mut self, params: N) -> NR { self.docs.open(params.text_document)?; + Ok(()) } - on DidCloseTextDocument(&mut self, params) { + fn DidCloseTextDocument(&mut self, params: N) -> NR { let url = self.docs.close(params.text_document)?; self.annotations.remove(&url); + Ok(()) } - on DidChangeTextDocument(&mut self, params) { - let url = self.docs.change(params.text_document, params.content_changes)?; + fn DidChangeTextDocument(&mut self, params: N) -> NR { + let url = self + .docs + .change(params.text_document, params.content_changes)?; self.annotations.remove(&url); + Ok(()) } - on DidChangeConfiguration(&mut self, params) { - if let Some(extools_dll) = params.settings["dreammaker"]["extoolsDLL"].as_str() { + fn DidChangeConfiguration(&mut self, params: N) -> NR { + if let Some(extools_dll) = params.settings["dreammaker"]["extoolsDLL"] + .as_str() + .filter(|path| !path.trim().is_empty()) + { self.extools_dll = Some(extools_dll.to_owned()); } - if let Some(debug_server_dll) = params.settings["dreammaker"]["debugServerDll"].as_str() { + if let Some(debug_server_dll) = params.settings["dreammaker"]["debugServerDll"] + .as_str() + .filter(|path| !path.trim().is_empty()) + { self.debug_server_dll = Some(debug_server_dll.to_owned()); } + Ok(()) } } @@ -2097,22 +2466,21 @@ fn url_to_path(url: &Url) -> Result { if url.scheme() != "file" { return Err(invalid_request("URI must have 'file' scheme")); } - url.to_file_path().map_err(|_| invalid_request("URI must be a valid path")) + url.to_file_path() + .map_err(|_| invalid_request("URI must be a valid path")) } fn path_to_url(path: PathBuf) -> Result { let formatted = path.display().to_string(); - Url::from_file_path(path).map_err(|_| invalid_request(format!( - "bad file path: {}", formatted, - ))) + Url::from_file_path(path).map_err(|_| invalid_request(format!("bad file path: {formatted}",))) } fn convert_severity(severity: dm::Severity) -> lsp_types::DiagnosticSeverity { match severity { - dm::Severity::Error => lsp_types::DiagnosticSeverity::Error, - dm::Severity::Warning => lsp_types::DiagnosticSeverity::Warning, - dm::Severity::Info => lsp_types::DiagnosticSeverity::Information, - dm::Severity::Hint => lsp_types::DiagnosticSeverity::Hint, + dm::Severity::Error => lsp_types::DiagnosticSeverity::ERROR, + dm::Severity::Warning => lsp_types::DiagnosticSeverity::WARNING, + dm::Severity::Info => lsp_types::DiagnosticSeverity::INFORMATION, + dm::Severity::Hint => lsp_types::DiagnosticSeverity::HINT, } } @@ -2141,10 +2509,10 @@ fn is_constructor_name(name: &str) -> bool { name == "New" || name == "init" || name == "Initialize" } -fn location_to_position(loc: dm::Location) -> lsp_types::Position { +fn location_to_position(loc: dm::Location) -> lsp_types::Position { lsp_types::Position { - line: loc.line.saturating_sub(1) as u64, - character: loc.column.saturating_sub(1) as u64, + line: loc.line.saturating_sub(1), + character: loc.column.saturating_sub(1) as u32, } } @@ -2154,7 +2522,10 @@ fn location_to_range(loc: dm::Location) -> lsp_types::Range { } fn span_to_range(range: std::ops::Range) -> lsp_types::Range { - lsp_types::Range::new(location_to_position(range.start), location_to_position(range.end)) + lsp_types::Range::new( + location_to_position(range.start), + location_to_position(range.end), + ) } fn issue_notification(params: T::Params) @@ -2163,7 +2534,7 @@ where T::Params: serde::Serialize, { let params = serde_json::to_value(params).expect("notification bad to_value"); - let request = Request::Single(Call::Notification(jsonrpc::Notification { + let request = jsonrpc::Request::Single(Call::Notification(jsonrpc::Notification { jsonrpc: VERSION, method: T::METHOD.to_owned(), params: value_to_params(params), diff --git a/crates/dm-langserver/src/symbol_search.rs b/crates/dm-langserver/src/symbol_search.rs index 2835b365..21c65529 100644 --- a/crates/dm-langserver/src/symbol_search.rs +++ b/crates/dm-langserver/src/symbol_search.rs @@ -16,16 +16,14 @@ impl Query { if !any_alphanumeric(query) { return None; } - Some(if query.starts_with('#') { - Query::Define(query[1..].to_lowercase()) - } else if query.starts_with("var/") { - let query = &query["var/".len()..]; + Some(if let Some(query) = query.strip_prefix('#') { + Query::Define(query.to_lowercase()) + } else if let Some(query) = query.strip_prefix("var/") { if !any_alphanumeric(query) { return None; } Query::Var(query.to_lowercase()) - } else if query.starts_with("proc/") { - let query = &query["proc/".len()..]; + } else if let Some(query) = query.strip_prefix("proc/") { if !any_alphanumeric(query) { return None; } @@ -55,39 +53,37 @@ impl Query { } pub fn matches_on_type(&self, _path: &str) -> bool { - match *self { - Query::Anything(_) | - Query::Proc(_) | - Query::Var(_) => true, - _ => false, - } + matches!(*self, Query::Anything(_) | Query::Proc(_) | Query::Var(_)) } pub fn matches_var(&self, name: &str) -> bool { match *self { - Query::Anything(ref q) | - Query::Var(ref q) => starts_with(name, q), + Query::Anything(ref q) | Query::Var(ref q) => starts_with(name, q), _ => false, } } pub fn matches_proc(&self, name: &str, _kind: dm::ast::ProcDeclKind) -> bool { match *self { - Query::Anything(ref q) | - Query::Proc(ref q) => starts_with(name, q), + Query::Anything(ref q) | Query::Proc(ref q) => starts_with(name, q), _ => false, } } } -fn simplify<'a>(s: &'a str) -> impl Iterator + Clone + 'a { - s.chars().flat_map(|c| c.to_lowercase()).filter(|c| c.is_alphanumeric()) +fn simplify(s: &str) -> impl Iterator + Clone + '_ { + s.chars() + .flat_map(|c| c.to_lowercase()) + .filter(|c| c.is_alphanumeric()) } // ignore case and underscores pub fn starts_with<'a>(fulltext: &'a str, query: &'a str) -> bool { let mut query_chars = simplify(query); - simplify(fulltext).zip(&mut query_chars).all(|(a, b)| a == b) && query_chars.next().is_none() + simplify(fulltext) + .zip(&mut query_chars) + .all(|(a, b)| a == b) + && query_chars.next().is_none() } pub fn contains<'a>(fulltext: &'a str, query: &'a str) -> bool { @@ -104,5 +100,7 @@ pub fn contains<'a>(fulltext: &'a str, query: &'a str) -> bool { } fn any_alphanumeric(text: &str) -> bool { - text.chars().flat_map(|c| c.to_lowercase()).any(|c| c.is_alphanumeric()) + text.chars() + .flat_map(|c| c.to_lowercase()) + .any(|c| c.is_alphanumeric()) } diff --git a/crates/dmdoc/Cargo.toml b/crates/dmdoc/Cargo.toml index c9864c2d..bbe6923a 100644 --- a/crates/dmdoc/Cargo.toml +++ b/crates/dmdoc/Cargo.toml @@ -1,22 +1,21 @@ [package] name = "dmdoc" -version = "1.4.1" -authors = ["Tad Hardesty "] -homepage = "https://github.com/SpaceManiac/SpacemanDMM/blob/master/src/dmdoc/README.md" -edition = "2018" +homepage = "https://github.com/SpaceManiac/SpacemanDMM/blob/master/crates/dmdoc/README.md" +version.workspace = true +authors.workspace = true +edition.workspace = true [dependencies] dreammaker = { path = "../dreammaker" } -pulldown-cmark = "0.7.0" -tera = "1.0.2" -serde = "1.0.71" -serde_derive = "1.0.27" -walkdir = "2.2.0" -git2 = { version = "0.13", default-features = false } +pulldown-cmark = "0.9.6" +walkdir = "2.5.0" +git2 = { version = "0.20.2", default-features = false } +maud = "0.27.0" +foldhash = "0.2.0" [dev-dependencies] -walkdir = "2.2.0" +walkdir = "2.5.0" [build-dependencies] -chrono = "0.4.0" -git2 = { version = "0.13", default-features = false } +chrono = "0.4.38" +git2 = { version = "0.20.2", default-features = false } diff --git a/crates/dmdoc/README.md b/crates/dmdoc/README.md index a89e62d6..2342d348 100644 --- a/crates/dmdoc/README.md +++ b/crates/dmdoc/README.md @@ -4,7 +4,7 @@ of the [BYOND] game engine. It produces simple static HTML files based on documented files, macros, types, procs, and vars. -[BYOND]: https://secure.byond.com/ +[BYOND]: https://www.byond.com/ If dmdoc is run in a Git repository, web links to source code are placed next to item headings in the generated output; otherwise, file and line numbers are diff --git a/crates/dmdoc/build.rs b/crates/dmdoc/build.rs index 71c828bd..df81ad5a 100644 --- a/crates/dmdoc/build.rs +++ b/crates/dmdoc/build.rs @@ -8,12 +8,12 @@ use std::path::PathBuf; fn main() { let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - let mut f = File::create(&out_dir.join("build-info.txt")).unwrap(); + let mut f = File::create(out_dir.join("build-info.txt")).unwrap(); if let Ok(commit) = read_commit() { - writeln!(f, "commit: {}", commit).unwrap(); + writeln!(f, "commit: {commit}").unwrap(); } - writeln!(f, "build date: {}", chrono::Utc::today()).unwrap(); + writeln!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap(); } fn read_commit() -> Result { diff --git a/crates/dmdoc/src/main.rs b/crates/dmdoc/src/main.rs index 51ee2a0b..b45333a1 100644 --- a/crates/dmdoc/src/main.rs +++ b/crates/dmdoc/src/main.rs @@ -1,43 +1,45 @@ //! A CLI tool to generate HTML documentation of DreamMaker codebases. #![forbid(unsafe_code)] extern crate dreammaker as dm; -extern crate pulldown_cmark; -extern crate tera; extern crate git2; +extern crate pulldown_cmark; extern crate walkdir; -#[macro_use] extern crate serde_derive; mod markdown; mod template; -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::io::{self, Write}; +use dm::ast::{InputType, ProcReturnType}; +use dm::objtree::ObjectTree; +use foldhash::HashSet; +use maud::{Markup, PreEscaped}; +use pulldown_cmark::{BrokenLink, CowStr}; +use std::collections::{BTreeMap, BTreeSet}; use std::fs::{self, File}; +use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; -use tera::Value; - use dm::docs::*; use markdown::DocBlock; +#[rustfmt::skip] const BUILD_INFO: &str = concat!( - "dmdoc ", env!("CARGO_PKG_VERSION"), " Copyright (C) 2017-2021 Tad Hardesty\n", + "dmdoc ", env!("CARGO_PKG_VERSION"), " Copyright (C) 2017-2025 Tad Hardesty\n", include_str!(concat!(env!("OUT_DIR"), "/build-info.txt")), "\n", "This program comes with ABSOLUTELY NO WARRANTY. This is free software,\n", "and you are welcome to redistribute it under the conditions of the GNU\n", "General Public License version 3.", ); -const DM_REFERENCE_BASE: &str = "https://secure.byond.com/docs/ref/#"; +const DM_REFERENCE_BASE: &str = "https://www.byond.com/docs/ref/#"; // ---------------------------------------------------------------------------- // Driver fn main() { if let Err(e) = main2() { - eprintln!("{}", e); + eprintln!("{e}"); std::process::exit(1); } } @@ -50,10 +52,10 @@ fn main2() -> Result<(), Box> { let mut dry_run = false; let mut args = std::env::args(); - let _ = args.next(); // skip executable name + let _ = args.next(); // skip executable name while let Some(arg) = args.next() { if arg == "-V" || arg == "--version" { - println!("{}", BUILD_INFO); + println!("{BUILD_INFO}"); return Ok(()); } else if arg == "-e" { environment = Some(args.next().expect("must specify a value for -e")); @@ -64,7 +66,7 @@ fn main2() -> Result<(), Box> { } else if arg == "--dry-run" { dry_run = true; } else { - return Err(format!("unknown argument: {}", arg).into()); + return Err(format!("unknown argument: {arg}").into()); } } @@ -77,18 +79,19 @@ fn main2() -> Result<(), Box> { Some(env) => env, None => { return Err("Unable to find a .dme file in this directory".into()); - } - } + }, + }, }; println!("parsing {}", environment.display()); let mut context = dm::Context::default(); - context.autodetect_config(&environment); context.set_print_severity(Some(dm::Severity::Error)); + context.autodetect_config(&environment); let mut pp = dm::preprocessor::Preprocessor::new(&context, environment.clone())?; let (objtree, module_docs) = { let indents = dm::indents::IndentProcessor::new(&context, &mut pp); - let parser = dm::parser::Parser::new(&context, indents); + let mut parser = dm::parser::Parser::new(&context, indents); + parser.enable_procs(); // for `set SpacemanDMM_return_type` parser.parse_with_module_docs() }; let define_history = pp.finalize(); @@ -96,10 +99,10 @@ fn main2() -> Result<(), Box> { println!("collating documented types"); if index_path.is_none() { - index_path = context.config().dmdoc.index_file.clone(); + index_path.clone_from(&context.config().dmdoc.index_file); } - let mut code_directories: std::collections::HashSet; + let mut code_directories: HashSet; if context.config().dmdoc.module_directories.is_empty() { // Any top-level directory which is `#include`d in the `.dme` (most // importantly "code", but also "_maps", "interface", and any downstream @@ -112,7 +115,13 @@ fn main2() -> Result<(), Box> { }); } else { // Use what the config specifies without any additional logic. - code_directories = context.config().dmdoc.module_directories.iter().map(std::ffi::OsString::from).collect(); + code_directories = context + .config() + .dmdoc + .module_directories + .iter() + .map(std::ffi::OsString::from) + .collect(); } // get a read on which types *have* docs @@ -121,7 +130,9 @@ fn main2() -> Result<(), Box> { // TODO: it would be nice if this was not a duplicate of below let mut own_docs = false; if !ty.docs.is_empty() { - own_docs = DocBlock::parse_with_title(&ty.docs.text(), None).1.has_description; + own_docs = DocBlock::parse_with_title(&ty.docs.text(), None) + .1 + .has_description; } let mut var_docs = BTreeSet::new(); @@ -140,10 +151,13 @@ fn main2() -> Result<(), Box> { } if own_docs || !var_docs.is_empty() || !proc_docs.is_empty() { - types_with_docs.insert(ty.get().path.as_str(), TypeHasDocs { - var_docs, - proc_docs, - }); + types_with_docs.insert( + ty.get().path.as_str(), + TypeHasDocs { + var_docs, + proc_docs, + }, + ); } }); @@ -176,203 +190,6 @@ fn main2() -> Result<(), Box> { let diagnostic_count: std::cell::Cell = Default::default(); let error_entity: std::cell::Cell> = Default::default(); let error_entity_put = |string: String| error_entity.set(Some(string)); - let error_entity_print = || { - diagnostic_count.set(diagnostic_count.get() + 1); - if let Some(name) = error_entity.take() { - eprintln!("{}:", name); - } - }; - - // (normalized, reference) -> (href, tooltip) - let broken_link_callback = &|_: &str, reference: &str| -> Option<(String, String)> { - // macros - if let Some(module) = macro_to_module_map.get(reference) { - return Some((format!("{}.html#define/{}", module, reference), reference.to_owned())); - } else if macro_exists.contains(reference) { - error_entity_print(); - eprintln!(" [{}]: macro not documented", reference); - return None; - } else if reference.ends_with(".dm") || reference.ends_with(".txt") || reference.ends_with(".md") { - let mod_path = module_path(reference.as_ref()); - if modules_which_exist.contains(&mod_path) { - return Some((format!("{}.html", mod_path), reference.to_owned())); - } - error_entity_print(); - eprintln!(" [{}]: module {}", reference, if Path::new(reference).exists() { "not documented" } else { "does not exist" }); - return None; - } - - // parse "proc" or "var" reference out - let mut ty_path = reference; - let mut proc_name = None; - let mut var_name = None; - let mut entity_exists = false; - if let Some(idx) = reference.find("/proc/") { - // `[/ty/proc/procname]` - let name = &reference[idx + "/proc/".len()..]; - proc_name = Some(name); - ty_path = &reference[..idx]; - if let Some(ty) = objtree.find(ty_path) { - entity_exists = ty.procs.contains_key(name); - } - } else if let Some(idx) = reference.find("/verb/") { - // `[/ty/verb/procname]` - let name = &reference[idx + "/verb/".len()..]; - proc_name = Some(name); - ty_path = &reference[..idx]; - if let Some(ty) = objtree.find(ty_path) { - // there are no builtin verbs - entity_exists = ty.procs.contains_key(name); - } - } else if let Some(idx) = reference.find("/var/") { - // `[/ty/var/varname]` - let name = &reference[idx + "/var/".len()..]; - var_name = Some(name); - ty_path = &reference[..idx]; - if let Some(ty) = objtree.find(ty_path) { - entity_exists = ty.vars.contains_key(name); - } - } else if let Some(_) = objtree.find(reference) { - entity_exists = true; - } else if let Some(idx) = reference.rfind('/') { - let (parent, rest) = (&reference[..idx], &reference[idx + 1..]); - if let Some(ty) = objtree.find(parent) { - if ty.procs.contains_key(rest) && !ty.vars.contains_key(rest) { - // correct `[/ty/procname]` to `[/ty/proc/procname]` - proc_name = Some(rest); - ty_path = parent; - error_entity_print(); - eprintln!(" [{}]: correcting to [{}/proc/{}]", reference, parent, rest); - entity_exists = true; - } else if ty.vars.contains_key(rest) { - // correct `[/ty/varname]` to `[/ty/var/varname]` - var_name = Some(rest); - ty_path = parent; - error_entity_print(); - eprintln!(" [{}]: correcting to [{}/var/{}]", reference, parent, rest); - entity_exists = true; - } - } - } else if objtree.root().vars.contains_key(reference) { - ty_path = ""; - var_name = Some(reference); - error_entity_print(); - eprintln!(" [{0}]: correcting to [/var/{0}]", reference); - entity_exists = true; - } - // else `[/ty]` - - // Determine external doc URL - let mut external_url = None; - if entity_exists { - // TODO: .location.is_builtins() is not the best way to find this out, - // for example if it's overridden in the DM code but not re-documented. - if let Some(ty) = objtree.find(ty_path) { - if let Some(var_name) = var_name { - if let Some(var) = ty.get_value(var_name) { - if var.location.is_builtins() { - external_url = Some(match var.docs.builtin_docs { - BuiltinDocs::None => format!("{}{}/var/{}", DM_REFERENCE_BASE, ty.path, var_name), - BuiltinDocs::ReferenceHash(hash) => format!("{}{}", DM_REFERENCE_BASE, hash), - }) - } - } - } else if let Some(proc_name) = proc_name { - if let Some(proc) = ty.get_proc(proc_name) { - if proc.location.is_builtins() { - external_url = Some(match proc.docs.builtin_docs { - BuiltinDocs::None => format!("{}{}/proc/{}", DM_REFERENCE_BASE, ty.path, proc_name), - BuiltinDocs::ReferenceHash(hash) => format!("{}{}", DM_REFERENCE_BASE, hash), - }) - } - } - } else { - if ty.location.is_builtins() { - external_url = Some(match ty.docs.builtin_docs { - BuiltinDocs::None => format!("{}{}", DM_REFERENCE_BASE, ty.path), - BuiltinDocs::ReferenceHash(hash) => format!("{}{}", DM_REFERENCE_BASE, hash), - }) - } - } - } - } - - let mut progress = String::new(); - let mut best = 0; - - if ty_path.is_empty() { - progress.push_str("/global"); - if let Some(info) = types_with_docs.get("") { - if let Some(proc_name) = proc_name { - if info.proc_docs.contains(proc_name) { - best = progress.len(); - } - } else if let Some(var_name) = var_name { - if info.var_docs.contains(var_name) { - best = progress.len(); - } - } else { - best = progress.len(); - } - } - } else { - for bit in ty_path.trim_start_matches('/').split('/') { - progress.push_str("/"); - progress.push_str(bit); - if let Some(info) = types_with_docs.get(progress.as_str()) { - if let Some(proc_name) = proc_name { - if info.proc_docs.contains(proc_name) { - best = progress.len(); - } - } else if let Some(var_name) = var_name { - if info.var_docs.contains(var_name) { - best = progress.len(); - } - } else { - best = progress.len(); - } - } - } - } - - if best > 0 { - use std::fmt::Write; - - if best < progress.len() { - error_entity_print(); - if entity_exists { - eprint!(" [{}]: not documented, guessing [{}", reference, &progress[..best]); - } else { - eprint!(" [{}]: unknown crosslink, guessing [{}", reference, &progress[..best]); - } - if let Some(proc_name) = proc_name { - let _ = eprint!("/proc/{}", proc_name); - } else if let Some(var_name) = var_name { - let _ = eprint!("/var/{}", var_name); - } - eprintln!("]"); - progress.truncate(best); - } - - let mut href = format!("{}.html", &progress[1..]); - if let Some(proc_name) = proc_name { - let _ = write!(href, "#proc/{}", proc_name); - } else if let Some(var_name) = var_name { - let _ = write!(href, "#var/{}", var_name); - } - Some((href, progress)) - } else if let Some(external) = external_url { - Some((external, reference.to_owned())) - } else { - error_entity_print(); - if entity_exists { - eprintln!(" [{}]: not documented", reference); - } else { - eprintln!(" [{}]: unknown crosslink", reference); - } - None - } - }; // if macros have docs, that counts as a module too for (range, (name, define)) in define_history.iter() { @@ -383,7 +200,7 @@ fn main2() -> Result<(), Box> { has_params = false; params = &[][..]; is_variadic = false; - } + }, dm::preprocessor::Define::Function { docs: dc, params: macro_params, @@ -394,20 +211,32 @@ fn main2() -> Result<(), Box> { has_params = true; params = macro_params; is_variadic = *variadic; - } + }, } macros_all += 1; if docs.is_empty() { continue; } - error_entity_put(format!("#define {}", name)); + error_entity_put(format!("#define {name}")); + let broken_link_callback = &mut |link: BrokenLink| -> Option<(CowStr, CowStr)> { + broken_link_fixer( + link, + ¯o_to_module_map, + ¯o_exists, + &diagnostic_count, + &error_entity, + &modules_which_exist, + &objtree, + &types_with_docs, + ) + }; let docs = DocBlock::parse(&docs.text(), Some(broken_link_callback)); let module = module_entry(&mut modules1, &context.file_path(range.start.file)); module.items_wip.push(( range.start.line, ModuleItem::Define { name, - teaser: docs.teaser().to_owned(), + teaser: PreEscaped(docs.teaser().0.to_owned()), }, )); module.defines.insert( @@ -425,18 +254,24 @@ fn main2() -> Result<(), Box> { // search the code tree for Markdown files for modules_path in code_directories { - for entry in walkdir::WalkDir::new(modules_path).into_iter().filter_entry(is_visible) { + for entry in walkdir::WalkDir::new(modules_path) + .into_iter() + .filter_entry(is_visible) + { let entry = entry?; let path = entry.path(); if let Some(buf) = read_as_markdown(path)? { if Some(path) != index_path.as_ref().map(Path::new) { - let module = module_entry(&mut modules1, &path); - module.items_wip.push((0, ModuleItem::DocComment(DocComment { - kind: CommentKind::Block, - target: DocTarget::EnclosingItem, - text: buf, - }))); + let module = module_entry(&mut modules1, path); + module.items_wip.push(( + 0, + ModuleItem::DocComment(DocComment { + kind: CommentKind::Block, + target: DocTarget::EnclosingItem, + text: buf, + }), + )); } } } @@ -445,8 +280,21 @@ fn main2() -> Result<(), Box> { // Incorporate the index file if requested. let mut index_docs = None; if let Some(index_path) = index_path { - let buf = read_as_markdown(index_path.as_ref())?.expect("file for --index must be .md or .txt"); - error_entity_put(index_path.to_owned()); + let buf = + read_as_markdown(index_path.as_ref())?.expect("file for --index must be .md or .txt"); + error_entity_put(index_path); + let broken_link_callback = &mut |link: BrokenLink| -> Option<(CowStr, CowStr)> { + broken_link_fixer( + link, + ¯o_to_module_map, + ¯o_exists, + &diagnostic_count, + &error_entity, + &modules_which_exist, + &objtree, + &types_with_docs, + ) + }; index_docs = Some(DocBlock::parse_with_title(&buf, Some(broken_link_callback))); } @@ -459,10 +307,7 @@ fn main2() -> Result<(), Box> { let mut parsed_type = ParsedType::default(); if context.config().dmdoc.use_typepath_names { - parsed_type.name = ty - .get() - .name() - .into(); + parsed_type.name = ty.get().name().into(); } else { parsed_type.name = ty .get() @@ -479,7 +324,20 @@ fn main2() -> Result<(), Box> { let mut substance = false; if !ty.docs.is_empty() { error_entity_put(ty.path.to_owned()); - let (title, block) = DocBlock::parse_with_title(&ty.docs.text(), Some(broken_link_callback)); + let broken_link_callback = &mut |link: BrokenLink| -> Option<(CowStr, CowStr)> { + broken_link_fixer( + link, + ¯o_to_module_map, + ¯o_exists, + &diagnostic_count, + &error_entity, + &modules_which_exist, + &objtree, + &types_with_docs, + ) + }; + let (title, block) = + DocBlock::parse_with_title(&ty.docs.text(), Some(broken_link_callback)); if let Some(title) = title { parsed_type.name = title.into(); } @@ -511,17 +369,30 @@ fn main2() -> Result<(), Box> { } error_entity_put(format!("{}/var/{}", ty.path, name)); + let broken_link_callback = &mut |link: BrokenLink| -> Option<(CowStr, CowStr)> { + broken_link_fixer( + link, + ¯o_to_module_map, + ¯o_exists, + &diagnostic_count, + &error_entity, + &modules_which_exist, + &objtree, + &types_with_docs, + ) + }; let block = DocBlock::parse(&var.value.docs.text(), Some(broken_link_callback)); // if the var is global, add it to the module tree if ty.is_root() { - let module = module_entry(&mut modules1, &context.file_path(var.value.location.file)); + let module = + module_entry(&mut modules1, &context.file_path(var.value.location.file)); module.items_wip.push(( var.value.location.line, ModuleItem::GlobalVar { name, - teaser: block.teaser().to_owned(), - } + teaser: PreEscaped(block.teaser().0.to_owned()), + }, )); } @@ -531,19 +402,23 @@ fn main2() -> Result<(), Box> { is_const: decl.var_type.flags.is_const(), is_tmp: decl.var_type.flags.is_tmp(), is_final: decl.var_type.flags.is_final(), - is_private: decl.var_type.flags.is_private(), - is_protected: decl.var_type.flags.is_protected(), + //is_private: decl.var_type.flags.is_private(), + //is_protected: decl.var_type.flags.is_protected(), path: &decl.var_type.type_path, + input_type: decl.var_type.input_type, }); - parsed_type.vars.insert(name, Var { - docs: block, - type_, - // but `decl` is only used if it's on this type - decl: if var.declaration.is_some() { "var" } else { "" }, - file: context.file_path(var.value.location.file), - line: var.value.location.line, - parent, - }); + parsed_type.vars.insert( + name, + Var { + docs: block, + type_, + // but `decl` is only used if it's on this type + decl: if var.declaration.is_some() { "var" } else { "" }, + file: context.file_path(var.value.location.file).to_owned(), + line: var.value.location.line, + parent, + }, + ); anything = true; substance = true; } @@ -567,35 +442,71 @@ fn main2() -> Result<(), Box> { } error_entity_put(format!("{}/proc/{}", ty.path, name)); + let broken_link_callback = &mut |link: BrokenLink| -> Option<(CowStr, CowStr)> { + broken_link_fixer( + link, + ¯o_to_module_map, + ¯o_exists, + &diagnostic_count, + &error_entity, + &modules_which_exist, + &objtree, + &types_with_docs, + ) + }; let block = DocBlock::parse(&proc_value.docs.text(), Some(broken_link_callback)); // if the proc is global, add it to the module tree if ty.is_root() { - let module = module_entry(&mut modules1, &context.file_path(proc_value.location.file)); + let module = + module_entry(&mut modules1, &context.file_path(proc_value.location.file)); module.items_wip.push(( proc_value.location.line, ModuleItem::GlobalProc { name, - teaser: block.teaser().to_owned(), - } + teaser: PreEscaped(block.teaser().0.to_owned()), + }, )); } + let return_type = proc + .declaration + .as_ref() + .map(|decl| &decl.return_type) + .filter(|ret| !ret.is_empty()) + .cloned() // could use Cow maybe + .or_else(|| { + proc_value + .code + .as_ref() + .and_then(find_return_type) + .map(ProcReturnType::TypePath) + }); + // add the proc to the type containing it - parsed_type.procs.insert(name, Proc { - docs: block, - params: proc_value.parameters.iter().map(|p| Param { - name: p.name.clone(), - type_path: format_type_path(&p.var_type.type_path), - }).collect(), - decl: match proc.declaration { - Some(ref decl) => decl.kind.name(), - None => "", + parsed_type.procs.insert( + name, + Proc { + docs: block, + params: proc_value + .parameters + .iter() + .map(|p| Param { + name: p.name.clone(), + type_path: format_type_path(&p.var_type.type_path), + input_type: p.input_type, + }) + .collect(), + decl: match proc.declaration { + Some(ref decl) => decl.kind.name(), + None => "", + }, + file: context.file_path(proc_value.location.file).to_owned(), + line: proc_value.location.line, + return_type, + parent, }, - file: context.file_path(proc_value.location.file), - line: proc_value.location.line, - parent, - }); + ); anything = true; substance = true; } @@ -608,14 +519,14 @@ fn main2() -> Result<(), Box> { ty.location.line, ModuleItem::Type { path: ty.get().pretty_path(), - teaser: block.teaser().to_owned(), - substance: substance, + teaser: PreEscaped(block.teaser().0.to_owned()), + substance, }, )); } if anything { - parsed_type.file = context.file_path(ty.location.file); + parsed_type.file = context.file_path(ty.location.file).to_owned(); parsed_type.line = ty.location.line; parsed_type.substance = substance; if substance { @@ -633,68 +544,94 @@ fn main2() -> Result<(), Box> { }); // collate all hrefable entities to use in autolinking - let all_type_names: Arc> = Arc::new(type_docs.iter() - .filter(|(_, v)| v.substance) - .map(|(&t, _)| t.to_owned()) - .collect()); + let all_type_names: Arc> = Arc::new( + type_docs + .iter() + .filter(|(_, v)| v.substance) + .map(|(&t, _)| t.to_owned()) + .collect(), + ); // finalize modules - let modules: BTreeMap<_, _> = modules1.into_iter().map(|(key, module1)| { - let Module1 { - htmlname, - orig_filename, - name, - teaser, - mut items_wip, - defines, - } = module1; - let mut module = Module { - htmlname, - orig_filename, - name, - teaser, - items: Vec::new(), - defines, - }; + let modules: BTreeMap<_, _> = modules1 + .into_iter() + .map(|(key, module1)| { + let Module1 { + htmlname, + orig_filename, + name, + teaser, + mut items_wip, + defines, + } = module1; + let mut module = Module { + htmlname, + orig_filename, + name, + teaser, + items: Vec::new(), + defines, + }; - let mut docs = DocCollection::default(); - let mut _first = true; - macro_rules! push_docs { () => { // oof - if !docs.is_empty() { - let doc = std::mem::replace(&mut docs, Default::default()); - if _first { - _first = false; - let (title, block) = DocBlock::parse_with_title(&doc.text(), Some(broken_link_callback)); - module.name = title; - module.teaser = block.teaser().to_owned(); - module.items.push(ModuleItem::Docs(block.html)); - } else { - module.items.push(ModuleItem::Docs(markdown::render(&doc.text(), Some(broken_link_callback)))); - } - } - }} - - error_entity_put(module.orig_filename.to_owned()); - let mut last_line = 0; - items_wip.sort_by_key(|&(line, _)| line); - for (line, item) in items_wip.drain(..) { - match item { - ModuleItem::DocComment(doc) => { - if line > last_line + 1 { - docs.push(DocComment::new(CommentKind::Line, DocTarget::EnclosingItem)); + let mut docs = DocCollection::default(); + let mut _first = true; + macro_rules! push_docs { + () => { + // oof + if !docs.is_empty() { + let doc = std::mem::take(&mut docs); + let broken_link_callback = + &mut |link: BrokenLink| -> Option<(CowStr, CowStr)> { + broken_link_fixer( + link, + ¯o_to_module_map, + ¯o_exists, + &diagnostic_count, + &error_entity, + &modules_which_exist, + &objtree, + &types_with_docs, + ) + }; + if _first { + _first = false; + let (title, block) = + DocBlock::parse_with_title(&doc.text(), Some(broken_link_callback)); + module.name = title; + module.teaser = PreEscaped(block.teaser().0.to_owned()); + module.items.push(ModuleItem::Docs(block.html)); + } else { + module.items.push(ModuleItem::Docs(markdown::render( + &doc.text(), + Some(broken_link_callback), + ))); + } } - docs.push(doc); - last_line = line; - }, - other => { - push_docs!(); - module.items.push(other); + }; + } + + error_entity_put(module.orig_filename.to_owned()); + let mut last_line = 0; + items_wip.sort_by_key(|&(line, _)| line); + for (line, item) in items_wip.drain(..) { + match item { + ModuleItem::DocComment(doc) => { + if line > last_line + 1 { + docs.push(DocComment::new(CommentKind::Line, DocTarget::EnclosingItem)); + } + docs.push(doc); + last_line = line; + }, + other => { + push_docs!(); + module.items.push(other); + }, } } - } - push_docs!(); - (key, module) - }).collect(); + push_docs!(); + (key, module) + }) + .collect(); print!("documenting {} modules, ", modules.len()); if macros_all == 0 { @@ -727,6 +664,7 @@ fn main2() -> Result<(), Box> { } } + /* // load tera templates println!("loading templates"); let mut tera = template::builtin()?; @@ -735,7 +673,7 @@ fn main2() -> Result<(), Box> { let linkify_typenames = all_type_names.clone(); tera.register_filter("linkify_type", move |value: &Value, _: &HashMap| { match *value { - tera::Value::String(ref s) => Ok(linkify_type(&linkify_typenames, s.split("/").skip_while(|b| b.is_empty())).into()), + tera::Value::String(ref s) => Ok(linkify_type(&linkify_typenames, s.split('/').skip_while(|b| b.is_empty())).into()), tera::Value::Array(ref a) => Ok(linkify_type(&linkify_typenames, a.iter().filter_map(|v| v.as_str())).into()), _ => Err("linkify_type() input must be string".into()), } @@ -765,6 +703,7 @@ fn main2() -> Result<(), Box> { _ => Err("substring() input must be string".into()), } }); + */ // render println!("saving static resources"); @@ -785,9 +724,10 @@ fn main2() -> Result<(), Box> { .unwrap_or(world_name); let mut git = Default::default(); if let Err(e) = git_info(&mut git) { - println!("incomplete git info: {}", e); + println!("incomplete git info: {e}"); } let env = &Environment { + all_type_names: &all_type_names, dmdoc: DmDoc { version: env!("CARGO_PKG_VERSION"), url: env!("CARGO_PKG_HOMEPAGE"), @@ -809,53 +749,43 @@ fn main2() -> Result<(), Box> { println!("rendering html"); { - #[derive(Serialize)] - struct Index<'a> { - env: &'a Environment<'a>, - html: Option<&'a str>, - modules: Vec>, - types: Vec>, - } - let mut index = create(&output_path.join("index.html"))?; - index.write_all(tera.render("dm_index.html", &tera::Context::from_serialize(Index { - env, - html: index_docs.as_ref().map(|(_, docs)| &docs.html[..]), - modules: build_index_tree(modules.iter().map(|(_path, module)| IndexTree { - htmlname: &module.htmlname, - full_name: &module.htmlname, - self_name: match module.name { - None => last_element(&module.htmlname), - Some(ref t) => t.as_str(), - }, - teaser: &module.teaser, - no_substance: false, - children: Vec::new(), - })), - types: build_index_tree(type_docs.iter().map(|(path, ty)| IndexTree { - htmlname: &ty.htmlname, - full_name: path, - self_name: if ty.name.is_empty() { - last_element(path) - } else { - &ty.name - }, - teaser: ty.docs.as_ref().map_or("", |d| d.teaser()), - no_substance: !ty.substance, - children: Vec::new(), - })), - })?)?.as_bytes())?; + index.write_all( + template::dm_index(&Index { + env, + html: index_docs + .as_ref() + .map(|(_, docs)| PreEscaped(docs.html.0.as_str())), + modules: build_index_tree(modules.values().map(|module| IndexTree { + htmlname: &module.htmlname, + full_name: &module.htmlname, + self_name: match module.name { + None => last_element(&module.htmlname), + Some(ref t) => t.as_str(), + }, + teaser: PreEscaped(&module.teaser.0), + no_substance: false, + children: Vec::new(), + })), + types: build_index_tree(type_docs.iter().map(|(path, ty)| IndexTree { + htmlname: ty.htmlname, + full_name: path, + self_name: if ty.name.is_empty() { + last_element(path) + } else { + &ty.name + }, + teaser: ty.docs.as_ref().map_or(PreEscaped(""), |d| d.teaser()), + no_substance: !ty.substance, + children: Vec::new(), + })), + }) + .0 + .as_bytes(), + )?; } - for (path, details) in modules.iter() { - #[derive(Serialize)] - struct ModuleArgs<'a> { - env: &'a Environment<'a>, - base_href: &'a str, - path: &'a str, - details: &'a Module<'a>, - } - + for (_path, details) in modules.iter() { let fname = format!("{}.html", details.htmlname); let mut base = String::new(); @@ -864,12 +794,16 @@ fn main2() -> Result<(), Box> { } let mut f = create(&output_path.join(&fname))?; - f.write_all(tera.render("dm_module.html", &tera::Context::from_serialize(ModuleArgs { - env, - base_href: &base, - path, - details, - })?)?.as_bytes())?; + f.write_all( + template::dm_module(&ModuleArgs { + env, + base_href: &base, + //path, + details, + }) + .0 + .as_bytes(), + )?; } for (path, details) in type_docs.iter() { @@ -877,15 +811,6 @@ fn main2() -> Result<(), Box> { continue; } - #[derive(Serialize)] - struct Type<'a> { - env: &'a Environment<'a>, - base_href: &'a str, - path: &'a str, - details: &'a ParsedType<'a>, - types: &'a BTreeMap<&'a str, ParsedType<'a>>, - } - let fname = format!("{}.html", details.htmlname); let mut base = String::new(); @@ -894,42 +819,314 @@ fn main2() -> Result<(), Box> { } let mut f = create(&output_path.join(&fname))?; - f.write_all(tera.render("dm_type.html", &tera::Context::from_serialize(Type { - env, - base_href: &base, - path, - details, - types: &type_docs, - })?)?.as_bytes())?; + f.write_all( + template::dm_type(&Type { + env, + base_href: &base, + path, + details, + //types: &type_docs, + }) + .0 + .as_bytes(), + )?; } Ok(()) } +fn find_return_type(code: &dm::ast::Block) -> Option> { + for stmt in code.iter() { + if let dm::ast::Statement::Setting { + name, + mode: dm::ast::SettingMode::Assign, + value, + } = &stmt.elem + { + if name.as_str() == "SpacemanDMM_return_type" { + if let Some(dm::ast::Term::Prefab(fab)) = value.as_term() { + let bits: Vec<_> = fab.path.iter().map(|(_, name)| name.to_owned()).collect(); + return Some(bits); + } + } + } else { + break; + } + } + None +} + +// reference & other captures -> (href, tooltip) +// this function's purpose is to prevent code copying in above closures +#[allow(clippy::too_many_arguments)] +fn broken_link_fixer<'str>( + link: BrokenLink, + macro_to_module_map: &BTreeMap<&str, String>, + macro_exists: &BTreeSet<&str>, + diagnostic_count: &std::cell::Cell, + error_entity: &std::cell::Cell>, + modules_which_exist: &BTreeSet, + objtree: &ObjectTree, + types_with_docs: &BTreeMap<&str, TypeHasDocs>, +) -> Option<(CowStr<'str>, CowStr<'str>)> { + let referie = link.reference.into_string(); + let reference = referie.as_str(); + let error_entity_print = || { + diagnostic_count.set(diagnostic_count.get() + 1); + if let Some(name) = error_entity.take() { + eprintln!("{name}:"); + } + }; + // macros + if let Some(module) = macro_to_module_map.get(reference) { + return Some(( + format!("{module}.html#define/{reference}").into(), + reference.to_owned().into(), + )); + } else if macro_exists.contains(reference) { + error_entity_print(); + eprintln!(" [{reference}]: macro not documented"); + return None; + } else if reference.ends_with(".dm") + || reference.ends_with(".txt") + || reference.ends_with(".md") + { + let mod_path = module_path(reference.as_ref()); + if modules_which_exist.contains(&mod_path) { + return Some(( + format!("{mod_path}.html").into(), + reference.to_owned().into(), + )); + } + error_entity_print(); + eprintln!( + " [{}]: module {}", + reference, + if Path::new(reference).exists() { + "not documented" + } else { + "does not exist" + } + ); + return None; + } + + // parse "proc" or "var" reference out + let mut ty_path = reference; + let mut proc_name = None; + let mut var_name = None; + let mut entity_exists = false; + if let Some(idx) = reference.find("/proc/") { + // `[/ty/proc/procname]` + let name = &reference[idx + "/proc/".len()..]; + proc_name = Some(name); + ty_path = &reference[..idx]; + if let Some(ty) = objtree.find(ty_path) { + entity_exists = ty.procs.contains_key(name); + } + } else if let Some(idx) = reference.find("/verb/") { + // `[/ty/verb/procname]` + let name = &reference[idx + "/verb/".len()..]; + proc_name = Some(name); + ty_path = &reference[..idx]; + if let Some(ty) = objtree.find(ty_path) { + // there are no builtin verbs + entity_exists = ty.procs.contains_key(name); + } + } else if let Some(idx) = reference.find("/var/") { + // `[/ty/var/varname]` + let name = &reference[idx + "/var/".len()..]; + var_name = Some(name); + ty_path = &reference[..idx]; + if let Some(ty) = objtree.find(ty_path) { + entity_exists = ty.vars.contains_key(name); + } + } else if objtree.find(reference).is_some() { + entity_exists = true; + } else if let Some(idx) = reference.rfind('/') { + let (parent, rest) = (&reference[..idx], &reference[idx + 1..]); + if let Some(ty) = objtree.find(parent) { + if ty.procs.contains_key(rest) && !ty.vars.contains_key(rest) { + // correct `[/ty/procname]` to `[/ty/proc/procname]` + proc_name = Some(rest); + ty_path = parent; + error_entity_print(); + eprintln!(" [{reference}]: correcting to [{parent}/proc/{rest}]"); + entity_exists = true; + } else if ty.vars.contains_key(rest) { + // correct `[/ty/varname]` to `[/ty/var/varname]` + var_name = Some(rest); + ty_path = parent; + error_entity_print(); + eprintln!(" [{reference}]: correcting to [{parent}/var/{rest}]"); + entity_exists = true; + } + } + } else if objtree.root().vars.contains_key(reference) { + ty_path = ""; + var_name = Some(reference); + error_entity_print(); + eprintln!(" [{reference}]: correcting to [/var/{reference}]"); + entity_exists = true; + } + // else `[/ty]` + + // Determine external doc URL + let mut external_url = None; + if entity_exists { + // TODO: .location.is_builtins() is not the best way to find this out, + // for example if it's overridden in the DM code but not re-documented. + if let Some(ty) = objtree.find(ty_path) { + if let Some(var_name) = var_name { + if let Some(var) = ty.get_value(var_name) { + if var.location.is_builtins() { + external_url = Some(match var.docs.builtin_docs { + BuiltinDocs::None => { + format!("{}{}/var/{}", DM_REFERENCE_BASE, ty.path, var_name) + }, + BuiltinDocs::ReferenceHash(hash) => { + format!("{DM_REFERENCE_BASE}{hash}") + }, + }) + } + } + } else if let Some(proc_name) = proc_name { + if let Some(proc) = ty.get_proc(proc_name) { + if proc.location.is_builtins() { + external_url = Some(match proc.docs.builtin_docs { + BuiltinDocs::None => { + format!("{}{}/proc/{}", DM_REFERENCE_BASE, ty.path, proc_name) + }, + BuiltinDocs::ReferenceHash(hash) => { + format!("{DM_REFERENCE_BASE}{hash}") + }, + }) + } + } + } else if ty.location.is_builtins() { + external_url = Some(match ty.docs.builtin_docs { + BuiltinDocs::None => format!("{}{}", DM_REFERENCE_BASE, ty.path), + BuiltinDocs::ReferenceHash(hash) => format!("{DM_REFERENCE_BASE}{hash}"), + }) + } + } + } + + let mut progress = String::new(); + let mut best = 0; + + if ty_path.is_empty() { + progress.push_str("/global"); + if let Some(info) = types_with_docs.get("") { + if let Some(proc_name) = proc_name { + if info.proc_docs.contains(proc_name) { + best = progress.len(); + } + } else if let Some(var_name) = var_name { + if info.var_docs.contains(var_name) { + best = progress.len(); + } + } else { + best = progress.len(); + } + } + } else { + for bit in ty_path.trim_start_matches('/').split('/') { + progress.push('/'); + progress.push_str(bit); + if let Some(info) = types_with_docs.get(progress.as_str()) { + if let Some(proc_name) = proc_name { + if info.proc_docs.contains(proc_name) { + best = progress.len(); + } + } else if let Some(var_name) = var_name { + if info.var_docs.contains(var_name) { + best = progress.len(); + } + } else { + best = progress.len(); + } + } + } + } + + if best > 0 { + use std::fmt::Write; + + if best < progress.len() { + error_entity_print(); + if entity_exists { + eprint!( + " [{}]: not documented, guessing [{}", + reference, + &progress[..best] + ); + } else { + eprint!( + " [{}]: unknown crosslink, guessing [{}", + reference, + &progress[..best] + ); + } + if let Some(proc_name) = proc_name { + eprint!("/proc/{proc_name}"); + } else if let Some(var_name) = var_name { + eprint!("/var/{var_name}"); + } + eprintln!("]"); + progress.truncate(best); + } + + let mut href = format!("{}.html", &progress[1..]); + if let Some(proc_name) = proc_name { + let _ = write!(href, "#proc/{proc_name}"); + } else if let Some(var_name) = var_name { + let _ = write!(href, "#var/{var_name}"); + } + Some((href.into(), progress.into())) + } else if let Some(external) = external_url { + Some((external.into(), reference.to_owned().into())) + } else { + error_entity_print(); + if entity_exists { + eprintln!(" [{reference}]: not documented"); + } else { + eprintln!(" [{reference}]: unknown crosslink"); + } + None + } +} + // ---------------------------------------------------------------------------- // Helpers fn module_path(path: &Path) -> String { let mut path = path.with_extension(""); - if path.file_name().map_or(false, |x| x.to_string_lossy().eq_ignore_ascii_case("README")) { + if path + .file_name() + .is_some_and(|x| x.to_string_lossy().eq_ignore_ascii_case("README")) + { path.pop(); } - path.display().to_string().replace("\\", "/") + path.display().to_string().replace('\\', "/") } -fn module_entry<'a, 'b>(modules: &'a mut BTreeMap>, path: &Path) -> &'a mut Module1<'b> { - modules.entry(module_path(path)).or_insert_with(|| { - let mut module = Module1::default(); - module.htmlname = module_path(path); - module.orig_filename = path.display().to_string().replace("\\", "/"); - module +fn module_entry<'a, 'b>( + modules: &'a mut BTreeMap>, + path: &Path, +) -> &'a mut Module1<'b> { + modules.entry(module_path(path)).or_insert_with(|| Module1 { + htmlname: module_path(path), + orig_filename: path.display().to_string().replace('\\', "/"), + ..Default::default() }) } fn is_visible(entry: &walkdir::DirEntry) -> bool { - entry.file_name() + entry + .file_name() .to_str() - .map(|s| !s.starts_with(".")) + .map(|s| !s.starts_with('.')) .unwrap_or(true) } @@ -941,14 +1138,17 @@ fn format_type_path(vec: &[String]) -> String { } } -fn linkify_type<'a, I: Iterator>(all_type_names: &BTreeSet, iter: I) -> String { +fn linkify_type<'a, I: Iterator>( + all_type_names: &BTreeSet, + iter: I, +) -> String { let mut output = String::new(); let mut all_progress = String::new(); let mut progress = String::new(); for bit in iter { - all_progress.push_str("/"); + all_progress.push('/'); all_progress.push_str(bit); - progress.push_str("/"); + progress.push('/'); progress.push_str(bit); if all_type_names.contains(&all_progress) { use std::fmt::Write; @@ -959,6 +1159,11 @@ fn linkify_type<'a, I: Iterator>(all_type_names: &BTreeSet &progress[1..] ); progress.clear(); + } else if all_progress == "/list" { + // Let `/list/datum` resolve to `/list/datum`. + output.push_str("/list"); + all_progress.clear(); + progress.clear(); } } output.push_str(&progress); @@ -981,7 +1186,7 @@ fn git_info(git: &mut Git) -> Result<(), git2::Error> { None => { println!("incomplete git info: malformed or non-utf8 name"); return Ok(()); - } + }, } }; } @@ -1000,45 +1205,45 @@ fn git_info(git: &mut Git) -> Result<(), git2::Error> { // check that the current revision is an ancestor of its remote let branch = repo.find_branch(req!(head.shorthand()), git2::BranchType::Local)?; if let Ok(Some(name)) = branch.name() { - git.branch = name.to_owned(); + name.clone_into(&mut git.branch); } let upstream = branch.upstream()?; let upstream_oid = upstream.get().peel_to_commit()?.id(); let upstream_name = req!(upstream.name()?); if repo.merge_base(head_oid, upstream_oid)? != head_oid { - println!("incomplete git info: HEAD is not an ancestor of {}", upstream_name); + println!("incomplete git info: HEAD is not an ancestor of {upstream_name}"); return Ok(()); } // figure out the remote URL, convert from SSH to HTTPS - let mut iter = upstream_name.splitn(2, "/"); + let mut iter = upstream_name.splitn(2, '/'); let remote_name = req!(iter.next()); if let Some(name) = iter.next() { - git.remote_branch = name.to_owned(); + name.clone_into(&mut git.remote_branch); } let remote = repo.find_remote(remote_name)?; let mut url = req!(remote.url()); - if url.ends_with("/") { + if url.ends_with('/') { url = &url[..url.len() - 1]; } if url.ends_with(".git") { url = &url[..url.len() - 4]; - if url.ends_with("/") { + if url.ends_with('/') { url = &url[..url.len() - 1]; } } if url.starts_with("https://") || url.starts_with("http://") { - git.web_url = url.to_owned(); + url.clone_into(&mut git.web_url); } else if url.starts_with("ssh://") { git.web_url = url.replace("ssh://", "https://"); } else { - let at = req!(url.find("@")); - let colon = req!(url.find(":")); + let at = req!(url.find('@')); + let colon = req!(url.find(':')); if colon >= at { git.web_url = format!("https://{}/{}", &url[at + 1..colon], &url[colon + 1..]); } else { - println!("incomplete git info: weird SSH path: {}", url); + println!("incomplete git info: weird SSH path: {url}"); } } Ok(()) @@ -1066,8 +1271,7 @@ fn read_as_markdown(path: &Path) -> std::io::Result> { } fn strip_propriety(name: &str) -> &str { - name - .trim_start_matches("\\proper") + name.trim_start_matches("\\proper") .trim_start_matches("\\improper") .trim_start() } @@ -1075,25 +1279,24 @@ fn strip_propriety(name: &str) -> &str { // ---------------------------------------------------------------------------- // Tree stuff -#[derive(Serialize)] struct IndexTree<'a> { - htmlname: &'a str, // href="{{htmlname}}.html" + htmlname: &'a str, // href="{{htmlname}}.html" full_name: &'a str, self_name: &'a str, - teaser: &'a str, + teaser: PreEscaped<&'a str>, no_substance: bool, children: Vec>, } fn build_index_tree<'a, I>(iter: I) -> Vec> where - I: IntoIterator>, + I: IntoIterator>, { let mut stack = vec![IndexTree { htmlname: "", full_name: "", self_name: "", - teaser: "", + teaser: PreEscaped(""), no_substance: false, children: Vec::new(), }]; @@ -1102,7 +1305,7 @@ where { let mut i = 1; let mut len = 0; - let mut bits = each.full_name.split("/").peekable(); + let mut bits = each.full_name.split('/').peekable(); if bits.peek() == Some(&"") { bits.next(); len += 1; @@ -1133,7 +1336,7 @@ where htmlname: "", full_name: &each.full_name[..len + bit.len()], self_name: bit, - teaser: "", + teaser: PreEscaped(""), no_substance: true, children: Vec::new(), }); @@ -1157,7 +1360,7 @@ fn combine(stack: &mut Vec, to: usize) { } fn last_element(path: &str) -> &str { - path.split("/").last().unwrap_or("") + path.split('/').next_back().unwrap_or("") } // ---------------------------------------------------------------------------- @@ -1175,7 +1378,7 @@ struct Module1<'a> { htmlname: String, orig_filename: String, name: Option, - teaser: String, + teaser: PreEscaped, items_wip: Vec<(u32, ModuleItem<'a>)>, defines: BTreeMap<&'a str, Define<'a>>, } @@ -1183,8 +1386,30 @@ struct Module1<'a> { // ---------------------------------------------------------------------------- // Templating structs -#[derive(Serialize)] +struct Index<'a> { + env: &'a Environment<'a>, + html: Option>, + modules: Vec>, + types: Vec>, +} + +struct ModuleArgs<'a> { + env: &'a Environment<'a>, + base_href: &'a str, + //path: &'a str, + details: &'a Module<'a>, +} + +struct Type<'a> { + env: &'a Environment<'a>, + base_href: &'a str, + path: &'a str, + details: &'a ParsedType<'a>, + //types: &'a BTreeMap<&'a str, ParsedType<'a>>, +} + struct Environment<'a> { + all_type_names: &'a BTreeSet, dmdoc: DmDoc, filename: &'a str, world_name: &'a str, @@ -1193,14 +1418,28 @@ struct Environment<'a> { git: Git, } -#[derive(Serialize)] +impl<'a> Environment<'a> { + fn linkify_type_str(&self, s: &str) -> Markup { + PreEscaped(linkify_type( + self.all_type_names, + s.split('/').skip_while(|b| b.is_empty()), + )) + } + + fn linkify_type_array(&self, a: &[String]) -> Markup { + PreEscaped(linkify_type( + self.all_type_names, + a.iter().map(|x| x.as_str()), + )) + } +} + struct DmDoc { version: &'static str, url: &'static str, build_info: &'static str, } -#[derive(Serialize)] struct Coverage { modules: usize, macros_documented: usize, @@ -1210,7 +1449,7 @@ struct Coverage { types_all: usize, } -#[derive(Serialize, Default)] +#[derive(Default)] struct Git { revision: String, branch: String, @@ -1219,7 +1458,7 @@ struct Git { } /// A parsed documented type. -#[derive(Default, Serialize)] +#[derive(Default)] struct ParsedType<'a> { name: std::borrow::Cow<'a, str>, parent_type: Option<&'a str>, @@ -1232,29 +1471,26 @@ struct ParsedType<'a> { line: u32, } -#[derive(Serialize)] struct Var<'a> { docs: DocBlock, decl: &'static str, - #[serde(rename="type")] type_: Option>, file: PathBuf, line: u32, parent: Option, } -#[derive(Serialize)] struct VarType<'a> { is_static: bool, is_const: bool, is_tmp: bool, is_final: bool, - is_private: bool, - is_protected: bool, + //is_private: bool, + //is_protected: bool, path: &'a [String], + input_type: InputType, } -#[derive(Serialize)] struct Proc { docs: DocBlock, decl: &'static str, @@ -1262,26 +1498,26 @@ struct Proc { file: PathBuf, line: u32, parent: Option, + return_type: Option, } -#[derive(Serialize)] struct Param { name: String, type_path: String, + input_type: Option, } /// Module struct exposed to templates. -#[derive(Default, Serialize)] +#[derive(Default)] struct Module<'a> { htmlname: String, orig_filename: String, name: Option, - teaser: String, + teaser: PreEscaped, items: Vec>, defines: BTreeMap<&'a str, Define<'a>>, } -#[derive(Serialize)] struct Define<'a> { docs: DocBlock, has_params: bool, @@ -1290,34 +1526,27 @@ struct Define<'a> { line: u32, } -#[derive(Serialize)] enum ModuleItem<'a> { // preparation - #[serde(skip)] DocComment(DocComment), // rendering - #[serde(rename="docs")] - Docs(String), - #[serde(rename="define")] + Docs(PreEscaped), Define { name: &'a str, - teaser: String, + teaser: PreEscaped, }, - #[serde(rename="type")] Type { path: &'a str, - teaser: String, + teaser: PreEscaped, substance: bool, }, - #[serde(rename="global_proc")] GlobalProc { name: &'a str, - teaser: String, + teaser: PreEscaped, }, - #[serde(rename="global_var")] GlobalVar { name: &'a str, - teaser: String, + teaser: PreEscaped, }, } diff --git a/crates/dmdoc/src/markdown.rs b/crates/dmdoc/src/markdown.rs index 96a1fd28..a0a44201 100644 --- a/crates/dmdoc/src/markdown.rs +++ b/crates/dmdoc/src/markdown.rs @@ -1,40 +1,47 @@ //! Parser for "doc-block" markdown documents. -use std::ops::Range; use std::collections::VecDeque; +use std::ops::Range; -use pulldown_cmark::{self, Parser, Tag, Event}; +use maud::PreEscaped; +use pulldown_cmark::{self, BrokenLinkCallback, Event, HeadingLevel, Parser, Tag}; -pub type BrokenLinkCallback<'a> = Option<&'a dyn Fn(&str, &str) -> Option<(String, String)>>; - -pub fn render(markdown: &str, broken_link_callback: BrokenLinkCallback) -> String { +pub fn render<'string>( + markdown: &'string str, + broken_link_callback: BrokenLinkCallback<'string, '_>, +) -> PreEscaped { let mut buf = String::new(); push_html(&mut buf, parser(markdown, broken_link_callback)); - buf + PreEscaped(buf) } /// A rendered markdown document with the teaser identified. -#[derive(Serialize)] pub struct DocBlock { - pub html: String, + pub html: PreEscaped, pub has_description: bool, teaser: Range, } impl DocBlock { - pub fn parse(markdown: &str, broken_link_callback: BrokenLinkCallback) -> Self { + pub fn parse<'string>( + markdown: &'string str, + broken_link_callback: BrokenLinkCallback<'string, '_>, + ) -> Self { parse_main(parser(markdown, broken_link_callback).peekable()) } - pub fn parse_with_title(markdown: &str, broken_link_callback: BrokenLinkCallback) -> (Option, Self) { + pub fn parse_with_title<'string>( + markdown: &'string str, + broken_link_callback: BrokenLinkCallback<'string, '_>, + ) -> (Option, Self) { let mut parser = parser(markdown, broken_link_callback).peekable(); ( - if let Some(&Event::Start(Tag::Heading(1))) = parser.peek() { + if let Some(&Event::Start(Tag::Heading(HeadingLevel::H1, _, _))) = parser.peek() { parser.next(); let mut pieces = Vec::new(); loop { match parser.next() { - None | Some(Event::End(Tag::Heading(1))) => break, + None | Some(Event::End(Tag::Heading(HeadingLevel::H1, _, _))) => break, Some(other) => pieces.push(other), } } @@ -49,16 +56,19 @@ impl DocBlock { ) } - pub fn teaser(&self) -> &str { - &self.html[self.teaser.clone()] + pub fn teaser(&self) -> PreEscaped<&str> { + PreEscaped(&self.html.0[self.teaser.clone()]) } } -fn parser<'a>(markdown: &'a str, broken_link_callback: BrokenLinkCallback<'a>) -> Parser<'a> { +fn parser<'string, 'func>( + markdown: &'string str, + broken_link_callback: BrokenLinkCallback<'string, 'func>, +) -> Parser<'string, 'func> { Parser::new_with_broken_link_callback( markdown, pulldown_cmark::Options::ENABLE_TABLES | pulldown_cmark::Options::ENABLE_STRIKETHROUGH, - broken_link_callback + broken_link_callback, ) } @@ -85,14 +95,21 @@ fn parse_main(mut parser: std::iter::Peekable) -> DocBlock { let has_description = parser.peek().is_some(); push_html(&mut html, parser); trim_right(&mut html); - DocBlock { html, teaser, has_description } + DocBlock { + html: PreEscaped(html), + teaser, + has_description, + } } -fn push_html<'a, I: IntoIterator>>(buf: &mut String, iter: I) { - pulldown_cmark::html::push_html(buf, HeadingLinker { - inner: iter.into_iter(), - output: Default::default(), - }); +fn push_html<'a, I: IntoIterator>>(buf: &mut String, iter: I) { + pulldown_cmark::html::push_html( + buf, + HeadingLinker { + inner: iter.into_iter(), + output: Default::default(), + }, + ); } fn trim_right(buf: &mut String) { @@ -107,7 +124,7 @@ struct HeadingLinker<'a, I> { output: VecDeque>, } -impl<'a, I: Iterator>> Iterator for HeadingLinker<'a, I> { +impl<'a, I: Iterator>> Iterator for HeadingLinker<'a, I> { type Item = Event<'a>; fn next(&mut self) -> Option> { @@ -116,23 +133,26 @@ impl<'a, I: Iterator>> Iterator for HeadingLinker<'a, I> { } let original = self.inner.next(); - if let Some(Event::Start(Tag::Heading(heading))) = original { + if let Some(Event::Start(Tag::Heading(heading, _, _))) = original { let mut text_buf = String::new(); - while let Some(event) = self.inner.next() { + for event in self.inner.by_ref() { if let Event::Text(ref text) = event { text_buf.push_str(text.as_ref()); } - if let Event::End(Tag::Heading(_)) = event { + if let Event::End(Tag::Heading(_, _, _)) = event { break; } self.output.push_back(event); } - self.output.push_back(Event::Html(format!("", heading).into())); - return Some(Event::Html(format!("", heading, slugify(&text_buf)).into())); + self.output + .push_back(Event::Html(format!("").into())); + return Some(Event::Html( + format!("<{} id=\"{}\">", heading, slugify(&text_buf)).into(), + )); } original } diff --git a/crates/dmdoc/src/template.rs b/crates/dmdoc/src/template.rs new file mode 100644 index 00000000..30c50c26 --- /dev/null +++ b/crates/dmdoc/src/template.rs @@ -0,0 +1,583 @@ +//! The built-in template. + +use std::path::Path; + +use dm::ast::{InputType, ProcReturnType}; +use maud::{display, html, Markup, PreEscaped, Render, DOCTYPE}; + +use crate::{markdown::DocBlock, Environment, Index, IndexTree, ModuleArgs, ModuleItem, Type}; + +pub(crate) fn base( + env: &Environment, + base_href: &str, + title: &dyn Render, + head: &dyn Render, + header: &dyn Render, + content: &dyn Render, +) -> Markup { + html! { + (DOCTYPE) + html lang="en" { + head { + meta charset="utf-8"; + @if !base_href.is_empty() { + base href=(base_href); + } + link rel="stylesheet" href="dmdoc.css"; + title { + (title) " - " (env.world_name) + } + (head) + } + body { + header { + a href="index.html" { (env.world_name) } " - " + a href="index.html#modules" { "Modules" } " - " + a href="index.html#types" { "Types" } + (header) + } + main { + (content) + } + footer { + (env.filename) + @if !env.git.revision.is_empty() { + " " + @if !env.git.web_url.is_empty() { + a href=(format!("{}/tree/{}", env.git.web_url, env.git.revision)) { + (env.git.revision[..7]) + } + } @else { + (env.git.revision) + } + @if !env.git.branch.is_empty() { + " (" + (env.git.branch) + @if !env.git.remote_branch.is_empty() && env.git.remote_branch != env.git.branch { + " → " (env.git.remote_branch) + } + ")" + } + " — " + @if !env.dmdoc.url.is_empty() { + a href=(env.dmdoc.url) { + "dmdoc " (env.dmdoc.version) + } + } @else { + "dmdoc " (env.dmdoc.version) + } + } + } + } + } + } +} + +fn teaser(block: &DocBlock, prefix: &str) -> Markup { + let teaser = block.teaser(); + html! { + @if !teaser.0.is_empty() { + (prefix) + (teaser) + } + } +} + +fn git_link(env: &Environment, file: &str, line: u32) -> Markup { + let z; + let title = if line == 0 { + file + } else { + z = format!("{file} {line}"); + &z + }; + let icon = html! { + img src="git.png" width="16" height="16" title=(title); + }; + html! { + @if !file.is_empty() && file != "(builtins)" { + " " + @if !env.git.web_url.is_empty() && !env.git.revision.is_empty() { + a href=( + if line == 0 { + format!( + "{}/blob/{}/{}", + env.git.web_url, + env.git.revision, + file + ) + } else { + format!( + "{}/blob/{}/{}#L{}", + env.git.web_url, + env.git.revision, + file, + line + ) + } + ) { + (icon) + } + } @else { + (icon) + } + } + } +} + +fn index_tree(elems: &[IndexTree], extra_class: &str) -> Markup { + html! { + ul .index-tree .(extra_class) { + @for tree in elems { + (index_tree_elem(tree, "")) + } + } + } +} + +fn index_tree_elem(tree: &IndexTree, prefix: &str) -> Markup { + html! { + @if tree.children.len() == 1 && tree.htmlname.is_empty() && tree.teaser.0.is_empty() { + (index_tree_elem(&tree.children[0], &format!("{}{}/", prefix, tree.self_name))) + } @else { + li .(if !tree.children.is_empty() { "has-children" } else { "" }) { + @if !prefix.is_empty() { + span class="no-substance" { (prefix) } + } + @if tree.htmlname.is_empty() { + span .(if tree.no_substance { "no-substance" } else { "" }) title=(tree.full_name) { (tree.self_name) } + } @else { + a href=(format!("{}.html", tree.htmlname)) title=(tree.full_name) { (tree.self_name) } + } + @if !tree.teaser.0.is_empty() { + " - " (tree.teaser) + } + @if !tree.children.is_empty() { + (index_tree(&tree.children, "")) + } + } + } + } +} + +fn percentage(amt: usize, total: usize) -> Markup { + if total > 0 { + PreEscaped(format!(", {:.1}%", (amt as f32) * 100.0 / (total as f32))) + } else { + Markup::default() + } +} + +pub(crate) fn dm_index(index: &Index) -> Markup { + let Index { + env, + html, + modules, + types, + } = index; + base( + env, + "", + &display("Index"), + &html! { + (maud::PreEscaped("\n\n")) + script src="dmdoc.js" {} + }, + &display(""), + &html! { + h1 { (env.title) } + @if let Some(html) = html { (html) } + + @if !modules.is_empty() { + h3 id="modules" { + "Modules " + aside { + "(" + (env.coverage.modules) " modules, " + (env.coverage.macros_documented)"/"(env.coverage.macros_all)" macros" + (percentage(env.coverage.macros_documented, env.coverage.macros_all)) + ")" + } + } + (index_tree(modules, "modules")) + } + + @if !types.is_empty() { + h3 id="types" { + "Types " + aside { + "(" + (env.coverage.types_detailed) " detailed/" + (env.coverage.types_documented) " documented/" + (env.coverage.types_all) " total" + (percentage(env.coverage.types_documented, env.coverage.types_all)) + ")" + } + } + (index_tree(types, "")) + } + }, + ) +} + +pub(crate) fn dm_module(module: &ModuleArgs) -> Markup { + let ModuleArgs { + env, + base_href, + details, + } = *module; + base( + env, + base_href, + &display(&details.orig_filename), + &display(""), + &html! { + @if !details.defines.is_empty() { + " — " + a href=(format!("{}.html#define", details.htmlname)) { "Define Details" } + } + }, + &html! { + h1 { + @if let Some(ref name) = details.name { + (name) " " + aside { + (details.orig_filename) + } + } @else { + (details.orig_filename) + } + (git_link(env, &details.orig_filename, 0)) + } + + table class="summary" cellspacing="0" { + @for item in details.items.iter() { + @match item { + ModuleItem::Docs(docs) => { + tr { + td colspan="2" { + (docs) + } + } + }, + ModuleItem::Define { name, teaser } => { + tr { + th { + a href=(format!("{}.html#define/{}", details.htmlname, name)) { (name) } + } + td { + (teaser) + } + } + }, + ModuleItem::Type { path, teaser, substance } => { + tr { + th { + @if *substance { + a href=(format!("{}.html", &path[1..])) { + (path) + } + } @else { + (env.linkify_type_str(path)) + } + } + td { + (teaser) + } + } + }, + ModuleItem::GlobalProc { name, teaser } => { + tr { + th { + "/proc/" + a href=(format!("global.html#proc/{}", name)) { + (name) + } + } + td { + (teaser) + } + } + }, + ModuleItem::GlobalVar { name, teaser } => { + tr { + th { + "/var/" + a href=(format!("global.html#var/{}", name)) { + (name) + } + } + td { + (teaser) + } + } + }, + ModuleItem::DocComment { .. } => {} + } + } + } + + @if !details.defines.is_empty() { + h2 id="define" { "Define Details" } + @for (name, define) in details.defines.iter() { + h3 id=(format!("define/{}", name)) { + aside class="declaration" { + "#define " + } + (name) + @if define.has_params { + aside { + "(" + @for (i, param) in define.params.iter().enumerate() { + @if i > 0 { + ", " + } + (param) + } + @if define.is_variadic { + " ..." + } + ")" + } + } + (git_link(env, &details.orig_filename, define.line)) + } + (define.docs.html) + } + } + }, + ) +} + +pub(crate) fn dm_type(ty: &Type) -> Markup { + let Type { + env, + base_href, + path, + details, + } = *ty; + base( + env, + base_href, + &display(path), + &display(""), + &html! { + @if !details.vars.is_empty() { + " — " + a href=(format!("{}.html#var", details.htmlname)) { "Var Details" } + } + @if !details.procs.is_empty() { + @if !details.vars.is_empty() { " - " } @else { " — " } + a href=(format!("{}.html#proc", details.htmlname)) { "Proc Details" } + } + }, + &html! { + h1 { + @if path == "global" { + "(global)" + } @else if !details.name.is_empty() { + (details.name) + " " + aside { + (env.linkify_type_str(path)) + } + } @else { + (env.linkify_type_str(path)) + } + @if let Some(parent_type) = details.parent_type { + aside { + " inherits " + (env.linkify_type_str(parent_type)) + } + } + (git_link(env, &details.file.to_string_lossy(), details.line)) + } + @if let Some(ref docs) = details.docs { + (docs.html) + } + + @if !details.vars.is_empty() || !details.procs.is_empty() { + table class="summary" cellspacing="0" { + @if !details.vars.is_empty() { + tr { td colspan="2" { h2 { "Vars" } } } + @for (name, var) in details.vars.iter() { + tr { + th { a href=(format!("{}.html#var/{}", details.htmlname, name)) { (name) } } + td { (teaser(&var.docs, "")) } + } + } + } + @if !details.procs.is_empty() { + tr { td colspan="2" { h2 { "Procs" } } } + @for (name, proc) in details.procs.iter() { + tr { + th { a href=(format!("{}.html#proc/{}", details.htmlname, name)) { (name) } } + td { (teaser(&proc.docs, "")) } + } + } + } + } + } + + @if !details.vars.is_empty() { + h2 id="var" { "Var Details" } + @for (name, var) in details.vars.iter() { + h3 id=(format!("var/{}", name)) { + @if !var.decl.is_empty() { + aside class="declaration" { (var.decl) " " } + } @else if let Some(ref parent) = var.parent { + aside class="parent" { + a title=(format!("/{}", parent)) href=(format!("{}.html#var/{}", parent, name)) { + "\u{2191}" // ↑ + } + } + } + (name) + @if let Some(ref ty) = var.type_ { + @if ty.is_static || ty.is_const || ty.is_tmp || ty.is_final || !ty.path.is_empty() || !ty.input_type.is_empty() { + " " + aside { + "\u{2013} " // – + @if ty.is_static { "/static" } + @if ty.is_const { "/const" } + @if ty.is_tmp { "/tmp" } + @if ty.is_final { "/final" } + (env.linkify_type_array(ty.path)) + @if !ty.input_type.is_empty() { + span class="as" { " as " } + (render_input_type(env, ty.input_type)) + } + } + } + } + (git_link(env, &var.file.to_string_lossy(), var.line)) + } + (var.docs.html) + } + } + + @if !details.procs.is_empty() { + h2 id="proc" { "Proc Details" } + @for (name, proc) in details.procs.iter() { + h3 id=(format!("proc/{}", name)) { + @if !proc.decl.is_empty() { + aside class="declaration" { (proc.decl) " " } + } @else if let Some(ref parent) = proc.parent { + aside class="parent" { + a title=(format!("/{}", parent)) href=(format!("{}.html#proc/{}", parent, name)) { + "\u{2191}" // ↑ + } + } + } + (name) + aside { + "(" + @for (i, param) in proc.params.iter().enumerate() { + @if i > 0 { ", " } + @if !param.type_path.is_empty() { + (env.linkify_type_str(¶m.type_path)) + "/" + } + (param.name) + @if let Some(input_type) = param.input_type { + @if !input_type.is_empty() { + span class="as" { " as " } + (render_input_type(env, input_type)) + } + } + } + ") " + @match &proc.return_type { + Some(ProcReturnType::InputType(i)) if !i.is_empty() => { + span class="as" { " as " } + (render_input_type(env, *i)) + }, + Some(ProcReturnType::TypePath(p)) => { + span class="as" { " as " } + (env.linkify_type_array(p)) + }, + _ => {}, + } + (git_link(env, &proc.file.to_string_lossy(), proc.line)) + } + } + (proc.docs.html) + } + } + }, + ) +} + +pub fn render_input_type(env: &Environment, input_type: InputType) -> Markup { + html! { + @for (i, &name) in matching_names(input_type).iter().enumerate() { + @if i > 0 { " | " } + @match name { + "mob" => (linkify_input_type(env, "mob", "/mob")), + "obj" => (linkify_input_type(env, "obj", "/obj")), + "turf" => (linkify_input_type(env, "turf", "/turf")), + "area" => (linkify_input_type(env, "area", "/area")), + //"icon" => (linkify_input_type(env, "icon", "/icon")), + //"sound" => (linkify_input_type(env, "sound", "/sound")), + "movable" => (linkify_input_type(env, "movable", "/atom/movable")), + "atom" => (linkify_input_type(env, "atom", "/atom")), + "list" => (linkify_input_type(env, "list", "/list")), + _ => (name), + } + } + } +} + +fn linkify_input_type(env: &Environment, show: &str, typepath: &str) -> Markup { + if env.all_type_names.contains(typepath) { + html! { + a href=(format!("{}.html", &typepath[1..])) { (show) } + } + } else { + html! { (show) } + } +} + +fn matching_names(mut input_type: InputType) -> Vec<&'static str> { + let mut result = Vec::with_capacity(input_type.bits().count_ones() as usize); + for &(name, value) in InputType::ENTRIES.iter().rev() { + if input_type.contains(value) { + input_type.remove(value); + result.push(name); + } + } + result.reverse(); + result +} + +pub fn save_resources(output_path: &Path) -> std::io::Result<()> { + #[cfg(debug_assertions)] + macro_rules! resources { + ($($name:expr,)*) => { + let env = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/template")); + $( + std::fs::copy(&env.join($name), &output_path.join($name))?; + )* + } + } + + #[cfg(not(debug_assertions))] + macro_rules! resources { + ($($name:expr,)*) => {{ + use std::io::Write; + $( + crate::create(&output_path.join($name))?.write_all(include_bytes!(concat!("template/", $name)))?; + )* + }} + } + + resources! { + "dmdoc.css", + "dmdoc.js", + "git.png", + } + + Ok(()) +} diff --git a/crates/dmdoc/src/template/base.html b/crates/dmdoc/src/template/base.html deleted file mode 100644 index a561caba..00000000 --- a/crates/dmdoc/src/template/base.html +++ /dev/null @@ -1,37 +0,0 @@ - - -{% block head %} - - {% if base_href %}{% endif %} - - {% block title %}{% endblock title %} - {{ env.world_name }} -{% endblock head %} - -
{% block header %} - {{ env.world_name }} - - Modules - - Types -{% endblock header %}
-
{% block content %}{% endblock content %}
-
{% block footer %} - {{ env.filename }} - {% if env.git.revision -%} - {%- if env.git.web_url -%} - {{ env.git.revision | substring(end=7) }} - {%- else -%} - {{ env.git.revision }} - {%- endif -%} - {%- if env.git.branch %} - ({{ env.git.branch }} - {%- if env.git.remote_branch and env.git.remote_branch != env.git.branch %} - → {{ env.git.remote_branch }} - {%- endif -%} - ){% endif %} - {%- endif %} — {% if env.dmdoc.url -%} - dmdoc {{ env.dmdoc.version }} - {%- else -%} - dmdoc {{ env.dmdoc.version }} - {%- endif -%} -{% endblock footer %}
- - diff --git a/crates/dmdoc/src/template/dm_index.html b/crates/dmdoc/src/template/dm_index.html deleted file mode 100644 index 4529ad87..00000000 --- a/crates/dmdoc/src/template/dm_index.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "base.html" %} -{% import "macros.html" as macros %} -{% block title %}Index{% endblock %} -{% block head %}{{ super() }} - - -{% endblock head %} -{% block content %} -

{{ env.title }}

-{% if html %}{{ html | safe }}{% endif %} - -{% if modules %} -

Modules -

-{{ macros::index_tree(elems=modules, extra_class="modules") }} -{% endif %} - -{% if types %} -

Types -

-{{ macros::index_tree(elems=types) }} -{% endif %} - -{% endblock content %} diff --git a/crates/dmdoc/src/template/dm_module.html b/crates/dmdoc/src/template/dm_module.html deleted file mode 100644 index 417953ea..00000000 --- a/crates/dmdoc/src/template/dm_module.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "base.html" %} -{% import "macros.html" as macros %} -{% block title %}{{ details.orig_filename }}{% endblock %} -{% block header -%} -{{ super() }} -{%- if details.defines %} — Define Details{% endif %} -{%- endblock %} -{% block content %} -

{% if details.name -%} - {{ details.name }} -{%- else -%} - {{ details.orig_filename | safe }} -{%- endif %} {{ macros::git_link(env=env, file=details.orig_filename) }}

- - - {%- for item in details.items %} - {% if item.docs -%} - - {%- elif item.define -%} - - {%- elif item.type -%} - - {%- elif item.global_proc -%} - - - {%- elif item.global_var -%} - - - {%- endif %} - {%- endfor -%} -
{{ item.docs | safe }}
{{item.define.name}}{{ item.define.teaser | safe }}
{% if item.type.substance -%} - {{item.type.path}} - {%- else -%} - {{ item.type.path | linkify_type | safe }} - {%- endif %}{{ item.type.teaser | safe }}
/proc/{{item.global_proc.name}}{{ item.global_proc.teaser | safe }}
/var/{{item.global_var.name}}{{ item.global_var.teaser | safe }}
- -{%- if details.defines -%} -

Define Details

-{% for name, define in details.defines -%} -

{{ name }} - {%- if define.has_params %} - - {%- endif -%} - {{ macros::git_link(env=env, item=define, file=details.orig_filename) }} -

- {{ define.docs.html | safe }} -{%- endfor -%} -{%- endif -%} -{% endblock content %} diff --git a/crates/dmdoc/src/template/dm_type.html b/crates/dmdoc/src/template/dm_type.html deleted file mode 100644 index 526f8322..00000000 --- a/crates/dmdoc/src/template/dm_type.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends "base.html" %} -{% import "macros.html" as macros %} -{% block title %}{{ path }}{% endblock %} -{% block header -%} -{{ super() }} -{%- if details.vars %} — Var Details{% endif %} -{%- if details.procs %}{% if details.vars %} - {% else %} — {% endif -%} - Proc Details{% endif %} -{%- endblock %} -{% block content %} -

{% if path == "(global)" -%} - (global) -{%- elif details.name -%} - {{ details.name }} -{%- else -%} - {{ path | linkify_type | safe }} -{%- endif %} -{%- if details.parent_type %}{% endif -%} -{{ macros::git_link(env=env, item=details) }}

- -{% if details.docs %}{{ details.docs.html | safe }}{% endif %} - -{%- if details.vars or details.procs -%} - - {%- if details.vars -%} - - {%- for name, var in details.vars %} - - {%- endfor %} - {%- endif -%} - - {%- if details.procs -%} - - {%- for name, proc in details.procs %} - - {%- endfor %} - {%- endif -%} -

Vars

{{ name }}{{ macros::teaser(block=var.docs) }}

Procs

{{ name }}{{ macros::teaser(block=proc.docs) }}
-{%- endif -%} - -{% if details.vars %} -

Var Details

- {%- for name, var in details.vars -%} -

- {%- if var.decl -%} - - {%- elif var.parent -%} - - {%- endif -%} - {{ name }} - {%- if var.type %} - - {%- endif -%} - {{ macros::git_link(env=env, item=var) }}

- {{ var.docs.html | safe }} - {%- endfor -%} -{% endif %} - -{%- if details.procs -%} -

Proc Details

- {%- for name, proc in details.procs -%} -

- {%- if proc.decl -%} - - {%- elif proc.parent -%} - - {%- endif -%} - {{ name }} -

- {{ proc.docs.html | safe }} - {%- endfor -%} -{%- endif -%} -{% endblock content %} diff --git a/crates/dmdoc/src/template/dmdoc.css b/crates/dmdoc/src/template/dmdoc.css index 367f005d..8307e907 100644 --- a/crates/dmdoc/src/template/dmdoc.css +++ b/crates/dmdoc/src/template/dmdoc.css @@ -45,7 +45,7 @@ aside.declaration, aside.parent { margin-right: -100px; right: 105px; } -aside.declaration { +aside.declaration, span.as { font-style: italic; } table.summary tr:first-child > td > :first-child { diff --git a/crates/dmdoc/src/template/macros.html b/crates/dmdoc/src/template/macros.html deleted file mode 100644 index edd1124c..00000000 --- a/crates/dmdoc/src/template/macros.html +++ /dev/null @@ -1,53 +0,0 @@ -{% macro teaser(block, prefix="") -%} - {%- if block -%} - {%- set teaser = block.html | safe | substring(start=block.teaser.start, end=block.teaser.end) -%} - {%- if teaser %}{{ prefix }}{{ teaser | safe }}{% endif -%} - {%- endif -%} -{%- endmacro teaser %} - -{% macro git_link(env, item=false, file=false) -%} - {% if item and item.file %} - {% set file = item.file %} - {% endif %} - {%- if file -%} - {%- if env.git.web_url and env.git.revision %} - - {%- endif %} - - {%- if env.git.web_url and env.git.revision %}{% endif %} - {%- endif -%} -{%- endmacro git_link %} - -{% macro index_tree(elems, extra_class="") -%} -
    - {% for tree in elems -%} - {{ self::index_tree_elem(tree=tree) }} - {% endfor -%} -
-{%- endmacro %} - -{% macro index_tree_elem(tree, prefix="") -%} - {%- if tree.children | length == 1 and not tree.htmlname and not tree.teaser -%} - {{ self::index_tree_elem(tree=tree.children[0], prefix=prefix ~ tree.self_name ~ "/") }} - {%- else -%} - - {%- if prefix -%} - {{prefix}} - {%- endif -%} - {%- if not tree.htmlname -%} - {{ tree.self_name }} - {%- else -%} - {{ tree.self_name }} - {%- endif %} - {%- if tree.teaser %} - {{ tree.teaser | safe }}{%- endif -%} - {% if tree.children %}{{ self::index_tree(elems=tree.children) }}{% endif -%} - - {%- endif -%} -{%- endmacro %} - -{% macro percentage(amt, total) -%} - {%- if total -%} - , {{ amt * 1000 / total | round / 10 }}% - {%- endif -%} -{%- endmacro %} diff --git a/crates/dmdoc/src/template/mod.rs b/crates/dmdoc/src/template/mod.rs deleted file mode 100644 index a5f8ab3c..00000000 --- a/crates/dmdoc/src/template/mod.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! The built-in template. - -use std::path::Path; -use tera::Tera; - -pub fn builtin() -> Result { - #[cfg(debug_assertions)] { - Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/template/*.html")) - } - - #[cfg(not(debug_assertions))] { - let mut tera = Tera::default(); - tera.add_raw_templates(vec![ - ("macros.html", include_str!("macros.html")), - ("base.html", include_str!("base.html")), - ("dm_index.html", include_str!("dm_index.html")), - ("dm_type.html", include_str!("dm_type.html")), - ("dm_module.html", include_str!("dm_module.html")), - ])?; - Ok(tera) - } -} - -pub fn save_resources(output_path: &Path) -> std::io::Result<()> { - #[cfg(debug_assertions)] - macro_rules! resources { - ($($name:expr,)*) => { - let env = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/template")); - $( - std::fs::copy(&env.join($name), &output_path.join($name))?; - )* - } - } - - #[cfg(not(debug_assertions))] - macro_rules! resources { - ($($name:expr,)*) => {{ - use std::io::Write; - $( - crate::create(&output_path.join($name))?.write_all(include_bytes!($name))?; - )* - }} - } - - resources! { - "dmdoc.css", - "dmdoc.js", - "git.png", - } - - Ok(()) -} diff --git a/crates/dmm-tools-cli/Cargo.toml b/crates/dmm-tools-cli/Cargo.toml index 08ae49a3..252407c5 100644 --- a/crates/dmm-tools-cli/Cargo.toml +++ b/crates/dmm-tools-cli/Cargo.toml @@ -1,25 +1,24 @@ [package] name = "dmm-tools-cli" -version = "1.3.1" -authors = ["Tad Hardesty "] description = "BYOND map rendering and analysis tools powered by SpacemanDMM" -edition = "2018" +version.workspace = true +authors.workspace = true +edition.workspace = true [[bin]] name = "dmm-tools" path = "src/main.rs" [dependencies] -structopt = "0.3.3" -structopt-derive = "0.4.0" -serde = "1.0.27" -serde_derive = "1.0.27" -serde_json = "1.0.9" -rayon = "1.0.0" +clap = { version = "4.5.20", features = ["derive"] } +serde = "1.0.213" +serde_derive = "1.0.213" +serde_json = "1.0.132" +rayon = "1.10.0" dreammaker = { path = "../dreammaker" } dmm-tools = { path = "../dmm-tools", features = ["png"] } -ahash = "0.7.6" +foldhash = "0.2.0" [build-dependencies] -chrono = "0.4.0" -git2 = { version = "0.13", default-features = false } +chrono = "0.4.38" +git2 = { version = "0.20.2", default-features = false } diff --git a/crates/dmm-tools-cli/build.rs b/crates/dmm-tools-cli/build.rs index 71c828bd..f827f259 100644 --- a/crates/dmm-tools-cli/build.rs +++ b/crates/dmm-tools-cli/build.rs @@ -8,12 +8,12 @@ use std::path::PathBuf; fn main() { let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - let mut f = File::create(&out_dir.join("build-info.txt")).unwrap(); + let mut f = File::create(out_dir.join("build-info.txt")).unwrap(); if let Ok(commit) = read_commit() { - writeln!(f, "commit: {}", commit).unwrap(); + writeln!(f, "commit: {commit}").unwrap(); } - writeln!(f, "build date: {}", chrono::Utc::today()).unwrap(); + write!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap(); } fn read_commit() -> Result { diff --git a/crates/dmm-tools-cli/src/main.rs b/crates/dmm-tools-cli/src/main.rs index ac8e9142..2ebf4f3d 100644 --- a/crates/dmm-tools-cli/src/main.rs +++ b/crates/dmm-tools-cli/src/main.rs @@ -1,46 +1,40 @@ //! CLI tools, including a map renderer, using the same backend as the editor. #![forbid(unsafe_code)] -#![doc(hidden)] // Don't interfere with lib docs. +#![doc(hidden)] // Don't interfere with lib docs. +extern crate clap; extern crate rayon; -extern crate structopt; extern crate serde; extern crate serde_json; -#[macro_use] extern crate serde_derive; +#[macro_use] +extern crate serde_derive; -extern crate dreammaker as dm; extern crate dmm_tools; +extern crate dreammaker as dm; -use std::collections::HashMap; +use foldhash::{HashMap, HashMapExt, HashSet}; use std::fmt; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicIsize, Ordering}; use std::sync::RwLock; -use std::collections::HashSet; +use clap::{Parser, Subcommand}; use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; -use structopt::StructOpt; use dm::objtree::ObjectTree; use dmm_tools::*; -use ahash::RandomState; - // ---------------------------------------------------------------------------- // Main driver fn main() { - let opt = Opt::from_clap(&Opt::clap() - .long_version(concat!( - env!("CARGO_PKG_VERSION"), "\n", - include_str!(concat!(env!("OUT_DIR"), "/build-info.txt")), - ).trim_end()) - .get_matches()); - + let opt = Opt::parse(); let mut context = Context::default(); - context.dm_context.set_print_severity(Some(dm::Severity::Error)); + context + .dm_context + .set_print_severity(Some(dm::Severity::Error)); rayon::ThreadPoolBuilder::new() .num_threads(opt.jobs) .build_global() @@ -73,16 +67,16 @@ impl Context { eprintln!("parsing {}", environment.display()); if let Some(parent) = environment.parent() { - self.icon_cache.set_icons_root(&parent); + self.icon_cache.set_icons_root(parent); } self.dm_context.autodetect_config(&environment); let pp = match dm::preprocessor::Preprocessor::new(&self.dm_context, environment) { Ok(pp) => pp, Err(e) => { - eprintln!("i/o error opening environment:\n{}", e); + eprintln!("i/o error opening environment:\n{e}"); std::process::exit(1); - } + }, }; let indents = dm::indents::IndentProcessor::new(&self.dm_context, pp); let parser = dm::parser::Parser::new(&self.dm_context, indents); @@ -90,87 +84,90 @@ impl Context { } } -#[derive(StructOpt, Debug)] -#[structopt(name="dmm-tools", -author="Copyright (C) 2017-2021 Tad Hardesty", -about="This program comes with ABSOLUTELY NO WARRANTY. This is free software, +#[derive(Parser, Debug)] +#[command( + name="dmm-tools", + version=concat!( + env!("CARGO_PKG_VERSION"), "\n", + include_str!(concat!(env!("OUT_DIR"), "/build-info.txt")) + ), + author="Copyright (C) 2017-2025 Tad Hardesty", + about="This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under the conditions of the GNU -General Public License version 3.")] +General Public License version 3.", +)] struct Opt { /// The environment file to operate under. - #[structopt(short="e", long="env")] + #[arg(short = 'e', long = "env")] environment: Option, - #[structopt(short="v", long="verbose")] + #[arg(short = 'v', long = "verbose")] #[allow(dead_code)] verbose: bool, /// Set the number of threads to be used for parallel execution when /// possible. A value of 0 will select automatically, and 1 will be serial. - #[structopt(long="jobs", default_value="1")] + #[arg(long = "jobs", default_value = "1")] jobs: usize, - #[structopt(subcommand)] + #[command(subcommand)] command: Command, } // ---------------------------------------------------------------------------- // Subcommands -#[derive(StructOpt, Debug)] +#[derive(Subcommand, Debug)] enum Command { /// Show information about the render-pass list. - #[structopt(name = "list-passes")] + #[command(name = "list-passes")] ListPasses { /// Output as JSON. - #[structopt(short="j", long="json")] + #[arg(short = 'j', long = "json")] json: bool, }, /// Build minimaps of the specified maps. - #[structopt(name = "minimap")] + #[command(name = "minimap")] Minimap { /// The output directory. - #[structopt(short="o", default_value="data/minimaps")] + #[arg(short = 'o', default_value = "data/minimaps")] output: String, /// Set the minimum x,y or x,y,z coordinate to act upon (1-indexed, inclusive). - #[structopt(long="min")] + #[arg(long = "min")] min: Option, /// Set the maximum x,y or x,y,z coordinate to act upon (1-indexed, inclusive). - #[structopt(long="max")] + #[arg(long = "max")] max: Option, /// Enable render-passes, or "all" to only exclude those passed to --disable. - #[structopt(long="enable", default_value="")] + #[arg(long = "enable", default_value = "")] enable: String, /// Disable render-passes, or "all" to only use those passed to --enable. - #[structopt(long="disable", default_value="")] + #[arg(long = "disable", default_value = "")] disable: String, /// Run output through pngcrush automatically. Requires pngcrush. - #[structopt(long="pngcrush")] + #[arg(long = "pngcrush")] pngcrush: bool, /// Run output through optipng automatically. Requires optipng. - #[structopt(long="optipng")] + #[arg(long = "optipng")] optipng: bool, /// The list of maps to process. files: Vec, }, /// List the differing coordinates between two maps. - #[structopt(name="diff-maps")] - DiffMaps { - left: String, - right: String, - }, + #[command(name = "diff-maps")] + DiffMaps { left: String, right: String }, /// Show metadata information about the map. - #[structopt(name="map-info")] + #[command(name = "map-info")] MapInfo { /// Output as JSON. - #[structopt(short="j", long="json")] + #[arg(short = 'j', long = "json")] json: bool, /// The list of maps to show info on. @@ -194,9 +191,17 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { let mut report = Vec::new(); for &render_passes::RenderPassInfo { - name, desc, default, new: _, - } in render_passes::RENDER_PASSES { - report.push(Pass { name, desc, default }); + name, + desc, + default, + new: _, + } in render_passes::RENDER_PASSES + { + report.push(Pass { + name, + desc, + default, + }); } output_json(&report); } else { @@ -219,17 +224,21 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { }, // -------------------------------------------------------------------- Command::Minimap { - ref output, min, max, ref enable, ref disable, ref files, - pngcrush, optipng, + ref output, + min, + max, + ref enable, + ref disable, + ref files, + pngcrush, + optipng, } => { context.objtree(opt); if context .dm_context .errors() .iter() - .filter(|e| e.severity() <= dm::Severity::Error) - .next() - .is_some() + .any(|e| e.severity() <= dm::Severity::Error) { println!("there were some parsing errors; render may be inaccurate") } @@ -241,9 +250,13 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { .. } = *context; - let render_passes = &dmm_tools::render_passes::configure(&context.dm_context.config().map_renderer, enable, disable); + let render_passes = &dmm_tools::render_passes::configure( + &context.dm_context.config().map_renderer, + enable, + disable, + ); let paths: Vec<&Path> = files.iter().map(|p| p.as_ref()).collect(); - let errors: RwLock> = Default::default(); + let errors: RwLock> = Default::default(); let perform_job = move |path: &Path| { let mut filename; @@ -263,7 +276,7 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { eprintln!("Failed to load {}:\n{}", path.display(), e); exit_status.fetch_add(1, Ordering::Relaxed); return; - } + }, }; let (dim_x, dim_y, dim_z) = map.dim_xyz(); @@ -279,24 +292,24 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { max.x = clamp(max.x, min.x, dim_x); max.y = clamp(max.y, min.y, dim_y); max.z = clamp(max.z, min.z, dim_z); - println!("{}rendering from {} to {}", prefix, min, max); + println!("{prefix}rendering from {min} to {max}"); let do_z_level = |z| { println!("{}generating z={}", prefix, 1 + z); let bump = Default::default(); let minimap_context = minimap::Context { - objtree: &objtree, + objtree, map: &map, level: map.z_level(z), min: (min.x - 1, min.y - 1), max: (max.x - 1, max.y - 1), - render_passes: &render_passes, + render_passes, errors: &errors, bump: &bump, }; let image = minimap::generate(minimap_context, icon_cache).unwrap(); if let Err(e) = std::fs::create_dir_all(output) { - eprintln!("Failed to create output directory {}:\n{}", output, e); + eprintln!("Failed to create output directory {output}:\n{e}"); exit_status.fetch_add(1, Ordering::Relaxed); return; } @@ -306,35 +319,41 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { path.file_stem().unwrap().to_string_lossy(), 1 + z ); - println!("{}saving {}", prefix, outfile); + println!("{prefix}saving {outfile}"); image.to_file(outfile.as_ref()).unwrap(); if pngcrush { - println!(" pngcrush {}", outfile); - let temp = format!("{}.temp", outfile); - assert!(std::process::Command::new("pngcrush") - .arg("-ow") - .arg(&outfile) - .arg(&temp) - .stderr(std::process::Stdio::null()) - .status() - .unwrap() - .success(), "pngcrush failed"); + println!(" pngcrush {outfile}"); + let temp = format!("{outfile}.temp"); + assert!( + std::process::Command::new("pngcrush") + .arg("-ow") + .arg(&outfile) + .arg(&temp) + .stderr(std::process::Stdio::null()) + .status() + .unwrap() + .success(), + "pngcrush failed" + ); } if optipng { - println!("{}optipng {}", prefix, outfile); - assert!(std::process::Command::new("optipng") - .arg(&outfile) - .stderr(std::process::Stdio::null()) - .status() - .unwrap() - .success(), "optipng failed"); + println!("{prefix}optipng {outfile}"); + assert!( + std::process::Command::new("optipng") + .arg(&outfile) + .stderr(std::process::Stdio::null()) + .status() + .unwrap() + .success(), + "optipng failed" + ); } }; if parallel { ((min.z - 1)..(max.z)).into_par_iter().for_each(do_z_level); } else { - ((min.z - 1)..(max.z)).into_iter().for_each(do_z_level); + ((min.z - 1)..(max.z)).for_each(do_z_level); } }; @@ -348,7 +367,8 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { }, // -------------------------------------------------------------------- Command::DiffMaps { - ref left, ref right, + ref left, + ref right, } => { use std::cmp::min; @@ -362,14 +382,16 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { let left_dims = left_map.dim_xyz(); let right_dims = right_map.dim_xyz(); if left_dims != right_dims { - println!(" different size: {:?} {:?}", left_dims, right_dims); + println!(" different size: {left_dims:?} {right_dims:?}"); } for z in 0..min(left_dims.2, right_dims.2) { for y in 0..min(left_dims.1, right_dims.1) { for x in 0..min(left_dims.0, right_dims.0) { - let left_tile = &left_map.dictionary[&left_map.grid[(z, left_dims.1 - y - 1, x)]]; - let right_tile = &right_map.dictionary[&right_map.grid[(z, right_dims.1 - y - 1, x)]]; + let left_tile = + &left_map.dictionary[&left_map.grid[(z, left_dims.1 - y - 1, x)]]; + let right_tile = + &right_map.dictionary[&right_map.grid[(z, right_dims.1 - y - 1, x)]]; if left_tile != right_tile { println!(" different tile: ({}, {}, {})", x + 1, y + 1, z + 1); } @@ -378,9 +400,7 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { } }, // -------------------------------------------------------------------- - Command::MapInfo { - json, ref files, - } => { + Command::MapInfo { json, ref files } => { if !json { eprintln!("non-JSON output is not yet supported"); } @@ -392,15 +412,18 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { num_keys: usize, } - let mut report = HashMap::with_hasher(RandomState::default()); + let mut report = HashMap::new(); for path in files.iter() { let path = std::path::Path::new(path); let map = dmm::Map::from_file(path).unwrap(); - report.insert(path, Map { - size: map.dim_xyz(), - key_length: map.key_length(), - num_keys: map.dictionary.len(), - }); + report.insert( + path, + Map { + size: map.dim_xyz(), + key_length: map.key_length(), + num_keys: map.dictionary.len(), + }, + ); } output_json(&report); }, @@ -412,8 +435,7 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) { let result = render_many(context, command); let stdout = std::io::stdout(); serde_json::to_writer(stdout.lock(), &result).unwrap(); - } - // -------------------------------------------------------------------- + }, } } @@ -443,7 +465,7 @@ impl std::str::FromStr for CoordArg { fn from_str(s: &str) -> Result { match s - .split(",") + .split(',') .map(|x| x.parse()) .collect::, std::num::ParseIntError>>() { @@ -551,9 +573,7 @@ fn render_many(context: &Context, command: RenderManyCommand) -> RenderManyComma .dm_context .errors() .iter() - .filter(|e| e.severity() <= dm::Severity::Error) - .next() - .is_some() + .any(|e| e.severity() <= dm::Severity::Error) { eprintln!("there were some parsing errors; render may be inaccurate") } @@ -563,72 +583,92 @@ fn render_many(context: &Context, command: RenderManyCommand) -> RenderManyComma ref exit_status, .. } = *context; - let render_passes = &dmm_tools::render_passes::configure_list(&context.dm_context.config().map_renderer, &command.enable, &command.disable); - let errors: RwLock> = Default::default(); + let render_passes = &dmm_tools::render_passes::configure_list( + &context.dm_context.config().map_renderer, + &command.enable, + &command.disable, + ); + let errors: RwLock> = Default::default(); // Prepare output directory. let output_directory = command.output_directory; if let Err(e) = std::fs::create_dir_all(&output_directory) { - eprintln!("failed to create output directory {}:\n{}", output_directory.display(), e); + eprintln!( + "failed to create output directory {}:\n{}", + output_directory.display(), + e + ); exit_status.fetch_add(1, Ordering::Relaxed); panic!(); } - // Iterate over the maps - let result_files: Vec<_> = command.files.into_par_iter().enumerate().map(|(file_idx, file)| { - eprintln!("{}: load {}", file_idx, file.path.display()); - let stem = file.path.file_stem().unwrap().to_string_lossy(); - let map = dmm::Map::from_file(&file.path).unwrap(); // TODO: error handling - let (dim_x, dim_y, dim_z) = map.dim_xyz(); + let result_files: Vec<_> = command + .files + .into_par_iter() + .enumerate() + .map(|(file_idx, file)| { + eprintln!("{}: load {}", file_idx, file.path.display()); + let stem = file.path.file_stem().unwrap().to_string_lossy(); + let map = dmm::Map::from_file(&file.path).unwrap(); // TODO: error handling + let (dim_x, dim_y, dim_z) = map.dim_xyz(); - // If `chunks` was not specified, render one chunk per z-level. - let chunks = file.chunks.unwrap_or_else(|| (1..=dim_z).map(|z| RenderManyChunk { - z, - min_x: None, - min_y: None, - max_x: None, - max_y: None, - }).collect()); + // If `chunks` was not specified, render one chunk per z-level. + let chunks = file.chunks.unwrap_or_else(|| { + (1..=dim_z) + .map(|z| RenderManyChunk { + z, + min_x: None, + min_y: None, + max_x: None, + max_y: None, + }) + .collect() + }); - let result_chunks: Vec<_> = chunks.into_par_iter().enumerate().map(|(chunk_idx, chunk)| { - eprintln!("{}/{}: render {:?}", file_idx, chunk_idx, chunk); + let result_chunks: Vec<_> = chunks + .into_par_iter() + .enumerate() + .map(|(chunk_idx, chunk)| { + eprintln!("{file_idx}/{chunk_idx}: render {chunk:?}"); - // Render the image. - let bump = Default::default(); - let minimap_context = minimap::Context { - objtree, - map: &map, - level: map.z_level(chunk.z - 1), - // Default and clamp to [1, max]. - min: (chunk.min_x.unwrap_or(1).max(1) - 1, chunk.min_y.unwrap_or(1).max(1) - 1), - max: (chunk.max_x.unwrap_or(dim_x).min(dim_x) - 1, chunk.max_y.unwrap_or(dim_y).min(dim_y) - 1), - render_passes, - errors: &errors, - bump: &bump, - }; - let image = minimap::generate(minimap_context, icon_cache).unwrap(); // TODO: error handling + // Render the image. + let bump = Default::default(); + let minimap_context = minimap::Context { + objtree, + map: &map, + level: map.z_level(chunk.z - 1), + // Default and clamp to [1, max]. + min: ( + chunk.min_x.unwrap_or(1).max(1) - 1, + chunk.min_y.unwrap_or(1).max(1) - 1, + ), + max: ( + chunk.max_x.unwrap_or(dim_x).min(dim_x) - 1, + chunk.max_y.unwrap_or(dim_y).min(dim_y) - 1, + ), + render_passes, + errors: &errors, + bump: &bump, + }; + let image = minimap::generate(minimap_context, icon_cache).unwrap(); // TODO: error handling - // Write it to file. - let filename = PathBuf::from(format!( - "{}_z{}_chunk{}.png", - stem, - chunk.z, - chunk_idx, - )); - eprintln!("{}/{}: save {}", file_idx, chunk_idx, filename.display()); - let outfile = output_directory.join(&filename); - image.to_file(&outfile).unwrap(); // TODO: error handling + // Write it to file. + let filename = + PathBuf::from(format!("{}_z{}_chunk{}.png", stem, chunk.z, chunk_idx,)); + eprintln!("{}/{}: save {}", file_idx, chunk_idx, filename.display()); + let outfile = output_directory.join(&filename); + image.to_file(&outfile).unwrap(); // TODO: error handling - RenderManyChunkResult { - filename, + RenderManyChunkResult { filename } + }) + .collect(); + + RenderManyFileResult { + chunks: result_chunks, } - }).collect(); - - RenderManyFileResult { - chunks: result_chunks, - } - }).collect(); + }) + .collect(); RenderManyCommandResult { files: result_files, diff --git a/crates/dmm-tools/Cargo.toml b/crates/dmm-tools/Cargo.toml index b7dfde5c..695fb59e 100644 --- a/crates/dmm-tools/Cargo.toml +++ b/crates/dmm-tools/Cargo.toml @@ -2,32 +2,37 @@ name = "dmm-tools" version = "0.1.0" authors = ["Tad Hardesty "] -edition = "2018" +edition = "2021" [dependencies] -inflate = "0.4.1" -ndarray = "0.15.3" -rand = "0.8.4" +inflate = "0.4.5" +ndarray = "0.15.6" +rand = "0.8.5" dreammaker = { path = "../dreammaker" } -lodepng = "3.0.0" -indexmap = "1.7.0" -ahash = "0.7.6" +lodepng = "3.10.7" +indexmap = "2.6.0" +foldhash = "0.2.0" +either = "1.13.0" [dependencies.bytemuck] -version = "1.5" +version = "1.19.0" features = ["derive"] [dependencies.bumpalo] -version = "3.0.0" +version = "3.16.0" features = ["collections"] [dependencies.png] -version = "0.17.2" +version = "0.17.14" optional = true [dependencies.gfx_core] version = "0.9.2" optional = true +[dependencies.gif] +version = "0.11.4" +optional = true + [dev-dependencies] -walkdir = "2.0.1" +walkdir = "2.5.0" diff --git a/crates/dmm-tools/examples/duplicate_icon_states.rs b/crates/dmm-tools/examples/duplicate_icon_states.rs index 85b1b907..546a5b47 100644 --- a/crates/dmm-tools/examples/duplicate_icon_states.rs +++ b/crates/dmm-tools/examples/duplicate_icon_states.rs @@ -1,13 +1,13 @@ -extern crate dreammaker as dm; extern crate dmm_tools; -extern crate walkdir; +extern crate dreammaker as dm; extern crate ndarray; +extern crate walkdir; -use std::path::Path; -use std::collections::HashMap; -use walkdir::{DirEntry, WalkDir}; use dmm_tools::dmi::*; +use foldhash::{HashMap, HashMapExt}; use ndarray::s; +use std::path::Path; +use walkdir::{DirEntry, WalkDir}; fn is_visible(entry: &DirEntry) -> bool { entry @@ -15,7 +15,7 @@ fn is_visible(entry: &DirEntry) -> bool { .file_name() .unwrap_or("".as_ref()) .to_str() - .map(|s| !s.starts_with(".")) + .map(|s| !s.starts_with('.')) .unwrap_or(true) } @@ -23,9 +23,9 @@ fn files_with_extension(ext: &str, mut f: F) { let dir = match std::env::var_os("TEST_DME") { Some(dme) => Path::new(&dme).parent().unwrap().to_owned(), None => { - println!("Set TEST_DME to check .{} files", ext); + println!("Set TEST_DME to check .{ext} files"); return; - } + }, }; for entry in WalkDir::new(dir).into_iter().filter_entry(is_visible) { let entry = entry.unwrap(); diff --git a/crates/dmm-tools/src/dmi.rs b/crates/dmm-tools/src/dmi.rs index af8916bd..8db801ff 100644 --- a/crates/dmm-tools/src/dmi.rs +++ b/crates/dmm-tools/src/dmi.rs @@ -2,22 +2,29 @@ //! //! Includes re-exports from `dreammaker::dmi`. +use bytemuck::Pod; use std::io; use std::path::Path; -use bytemuck::Pod; -use lodepng::{self, RGBA, Decoder, ColorType}; +use lodepng::{self, ColorType, Decoder, RGBA}; use ndarray::Array2; pub use dm::dmi::*; use std::ops::{Index, IndexMut}; -type Rect = (u32, u32, u32, u32); +/// Absolute x and y. +pub type Coordinate = (u32, u32); +/// Start x, Start y, End x, End y - relative to Coordinate. +pub type Rect = (u32, u32, u32, u32); // ---------------------------------------------------------------------------- // Icon file and metadata handling +#[cfg(all(feature = "png", feature = "gif"))] +pub mod render; + /// An image with associated DMI metadata. +#[derive(Debug)] pub struct IconFile { /// The icon's metadata. pub metadata: Metadata, @@ -27,7 +34,11 @@ pub struct IconFile { impl IconFile { pub fn from_file(path: &Path) -> io::Result { - let (bitmap, metadata) = Metadata::from_file(path)?; + Self::from_bytes(&std::fs::read(path)?) + } + + pub fn from_bytes(data: &[u8]) -> io::Result { + let (bitmap, metadata) = Metadata::from_bytes(data)?; Ok(IconFile { metadata, image: Image::from_rgba(bitmap), @@ -35,7 +46,7 @@ impl IconFile { } #[inline] - pub fn rect_of(&self, icon_state: &str, dir: Dir) -> Option { + pub fn rect_of(&self, icon_state: &StateIndex, dir: Dir) -> Option { self.metadata.rect_of(self.image.width, icon_state, dir, 0) } @@ -49,9 +60,18 @@ impl IconFile { self.metadata.height, ) } + + pub fn get_icon_state(&self, icon_state: &StateIndex) -> io::Result<&State> { + self.metadata.get_icon_state(icon_state).ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + format!("icon_state {icon_state} not found"), + ) + }) + } } -#[derive(Default, Clone, Copy, Pod, Zeroable, Eq, PartialEq)] +#[derive(Default, Debug, Clone, Copy, Pod, Zeroable, Eq, PartialEq)] #[repr(C)] pub struct Rgba8 { pub r: u8, @@ -92,6 +112,7 @@ impl IndexMut for Rgba8 { // Image manipulation /// A two-dimensional RGBA image. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Image { pub width: u32, pub height: u32, @@ -103,9 +124,7 @@ impl Image { Image { width, height, - data: { - Array2::default((width as usize, height as usize)) - }, + data: { Array2::default((width as usize, height as usize)) }, } } @@ -122,18 +141,17 @@ impl Image { } } - /// Read an `Image` from a file. + /// Read an `Image` from a [u8] array. /// - /// Prefer to call `IconFile::from_file`, which can read both metadata and + /// Prefer to call `IconFile::from_bytes`, which can read both metadata and /// image contents at one time. - pub fn from_file(path: &Path) -> io::Result { - let path = &::dm::fix_case(path); + pub fn from_bytes(data: &[u8]) -> io::Result { let mut decoder = Decoder::new(); decoder.info_raw_mut().colortype = ColorType::RGBA; decoder.info_raw_mut().set_bitdepth(8); decoder.read_text_chunks(false); decoder.remember_unknown_chunks(false); - let bitmap = match decoder.decode_file(path) { + let bitmap = match decoder.decode(data) { Ok(::lodepng::Image::RGBA(bitmap)) => bitmap, Ok(_) => return Err(io::Error::new(io::ErrorKind::InvalidData, "not RGBA")), Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), @@ -142,21 +160,45 @@ impl Image { Ok(Image::from_rgba(bitmap)) } + /// Read an `Image` from a file. + /// + /// Prefer to call `IconFile::from_file`, which can read both metadata and + /// image contents at one time. + pub fn from_file(path: &Path) -> io::Result { + let path = &::dm::fix_case(path); + Self::from_bytes(&std::fs::read(path)?) + } + + pub fn clear(&mut self) { + self.data.fill(Default::default()) + } + #[cfg(feature = "png")] - pub fn to_file(&self, path: &Path) -> io::Result<()> { - use std::fs::File; - - let mut encoder = png::Encoder::new(File::create(path)?, self.width, self.height); - encoder.set_color(::png::ColorType::Rgba); - encoder.set_depth(::png::BitDepth::Eight); - let mut writer = encoder.write_header()?; - // TODO: metadata with write_chunk() - - writer.write_image_data(bytemuck::cast_slice(self.data.as_slice().unwrap()))?; + pub fn to_write(&self, writer: W) -> io::Result<()> { + { + let mut encoder = png::Encoder::new(writer, self.width, self.height); + encoder.set_color(::png::ColorType::Rgba); + encoder.set_depth(::png::BitDepth::Eight); + let mut writer = encoder.write_header()?; + // TODO: metadata with write_chunk() + writer.write_image_data(bytemuck::cast_slice(self.data.as_slice().unwrap()))?; + } Ok(()) } - pub fn composite(&mut self, other: &Image, pos: (u32, u32), crop: Rect, color: [u8; 4]) { + #[cfg(feature = "png")] + pub fn to_file(&self, path: &Path) -> io::Result<()> { + self.to_write(std::fs::File::create(path)?) + } + + #[cfg(feature = "png")] + pub fn to_bytes(&self) -> io::Result> { + let mut vector = Vec::new(); + self.to_write(&mut vector)?; + Ok(vector) + } + + pub fn composite(&mut self, other: &Image, pos: Coordinate, crop: Rect, color: [u8; 4]) { let other_dat = other.data.as_slice().unwrap(); let self_dat = self.data.as_slice_mut().unwrap(); let mut sy = crop.1; @@ -166,13 +208,10 @@ impl Image { let src = other_dat[(sy * other.width + sx) as usize]; macro_rules! tint { ($i:expr) => { - mul255( - src[$i], - color[$i], - ) + mul255(src[$i], color[$i]) }; } - let mut dst = &mut self_dat[(y * self.width + x) as usize]; + let dst = &mut self_dat[(y * self.width + x) as usize]; let src_tint = Rgba8::new(tint!(0), tint!(1), tint!(2), tint!(3)); // out_A = src_A + dst_A (1 - src_A) @@ -189,7 +228,7 @@ impl Image { dst[i] = 0; } } - dst.a = out_a as u8; + dst.a = out_a; sx += 1; } diff --git a/crates/dmm-tools/src/dmi/render.rs b/crates/dmm-tools/src/dmi/render.rs new file mode 100644 index 00000000..19596b44 --- /dev/null +++ b/crates/dmm-tools/src/dmi/render.rs @@ -0,0 +1,210 @@ +use super::*; +use either::Either; +use gif::DisposalMethod; + +static NO_TINT: [u8; 4] = [0xff, 0xff, 0xff, 0xff]; + +/// Used to render an IconFile to a .gif/.png easily. +#[derive(Debug)] +pub struct IconRenderer<'a> { + /// The IconFile we render from. + source: &'a IconFile, +} + +/// [`IconRenderer::render`] will return this to indicate if it wrote to the stream using +/// [`gif::Encoder`] or `[`png::Encoder`]. +#[derive(Debug, Clone, Copy)] +pub enum RenderType { + Png, + Gif, +} + +#[derive(Debug)] +pub struct RenderStateGuard<'a> { + pub render_type: RenderType, + renderer: &'a IconRenderer<'a>, + state: &'a State, +} + +impl<'a> RenderStateGuard<'a> { + pub fn render(self, target: W) -> io::Result<()> { + match self.render_type { + RenderType::Png => self.renderer.render_to_png(self.state, target), + RenderType::Gif => self.renderer.render_gif(self.state, target), + } + } +} + +/// Public API +impl<'a> IconRenderer<'a> { + pub fn new(source: &'a IconFile) -> Self { + Self { source } + } + + /// Renders with either [`gif::Encoder`] or [`png::Encoder`] depending on whether the icon state is animated + /// or not. + /// Returns a [`RenderType`] to help you determine how to treat the written data. + pub fn prepare_render(&self, icon_state: &StateIndex) -> io::Result { + self.prepare_render_state(self.source.get_icon_state(icon_state)?) + } + + /// This is here so that duplicate icon states can be handled by not relying on the btreemap + /// of state names in [`Metadata`]. + pub fn prepare_render_state(&'a self, icon_state: &'a State) -> io::Result { + match icon_state.is_animated() { + false => Ok(RenderStateGuard { + renderer: self, + state: icon_state, + render_type: RenderType::Png, + }), + true => Ok(RenderStateGuard { + renderer: self, + state: icon_state, + render_type: RenderType::Gif, + }), + } + } + + /// Instead of writing to a file, this gives a Vec of each frame/dir as it would be composited + /// for a file. + pub fn render_to_images(&self, icon_state: &StateIndex) -> io::Result> { + let state = self.source.get_icon_state(icon_state)?; + Ok(self.render_frames(state)) + } +} + +/// Private helpers +impl<'a> IconRenderer<'a> { + /// Helper for render_to_images- not used for render_gif because it's less efficient. + fn render_frames(&self, icon_state: &State) -> Vec { + let frames = match &icon_state.frames { + Frames::One => 1, + Frames::Count(count) => *count, + Frames::Delays(delays) => delays.len(), + }; + let mut canvas = self.get_canvas(icon_state.dirs); + let mut vec = Vec::new(); + let range = if icon_state.rewind { + Either::Left((0..frames).chain((0..frames).rev())) + } else { + Either::Right(0..frames) + }; + for frame in range { + self.render_dirs(icon_state, &mut canvas, frame as u32); + vec.push(canvas.clone()); + canvas.clear(); + } + vec + } + + /// Returns a new canvas of the appropriate size + fn get_canvas(&self, dirs: Dirs) -> Image { + match dirs { + Dirs::One => Image::new_rgba(self.source.metadata.width, self.source.metadata.height), + Dirs::Four => { + Image::new_rgba(self.source.metadata.width * 4, self.source.metadata.height) + }, + Dirs::Eight => { + Image::new_rgba(self.source.metadata.width * 8, self.source.metadata.height) + }, + } + } + + /// Gives a [`Vec`] of each [`Dir`] matching our [`Dirs`] setting, + /// in the same order BYOND uses. + fn ordered_dirs(dirs: Dirs) -> Vec { + match dirs { + Dirs::One => [Dir::South].to_vec(), + Dirs::Four => [Dir::South, Dir::North, Dir::East, Dir::West].to_vec(), + Dirs::Eight => [ + Dir::South, + Dir::North, + Dir::East, + Dir::West, + Dir::Southeast, + Dir::Southwest, + Dir::Northeast, + Dir::Northwest, + ] + .to_vec(), + } + } + + /// Renders each direction to the same canvas, offsetting them to the right + fn render_dirs(&self, icon_state: &State, canvas: &mut Image, frame: u32) { + for (dir_no, dir) in Self::ordered_dirs(icon_state.dirs).iter().enumerate() { + let frame_idx = icon_state.index_of_frame(*dir, frame as u32); + let frame_rect = self.source.rect_of_index(frame_idx); + canvas.composite( + &self.source.image, + (self.source.metadata.width * (dir_no as u32), 0), + frame_rect, + NO_TINT, + ); + } + } + + /// Renders the whole file to a gif, animated states becoming frames + fn render_gif(&self, icon_state: &State, target: W) -> io::Result<()> { + if !icon_state.is_animated() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Tried to render gif with one frame", + )); + } + + let (frames, delays) = match &icon_state.frames { + Frames::Count(frames) => (*frames, None), + Frames::Delays(delays) => (delays.len(), Some(delays)), + _ => unreachable!(), + }; + + let mut canvas = self.get_canvas(icon_state.dirs); + + let mut encoder = gif::Encoder::new(target, canvas.width as u16, canvas.height as u16, &[]) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e}")))?; + + encoder + .set_repeat(gif::Repeat::Infinite) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e}")))?; + + let range = if icon_state.rewind { + Either::Left((0..frames).chain((0..frames).rev())) + } else { + Either::Right(0..frames) + }; + + for frame in range { + self.render_dirs(icon_state, &mut canvas, frame as u32); + + let mut pixels = bytemuck::cast_slice(canvas.data.as_slice().unwrap()).to_owned(); + let mut gif_frame = + gif::Frame::from_rgba(canvas.width as u16, canvas.height as u16, &mut pixels); + // gif::Frame delays are measured in "Frame delay in units of 10 ms." + // aka centiseconds. We're measuring in BYOND ticks, aka deciseconds. + // And it's a u16 for some reason so we just SHRUG and floor it. + gif_frame.delay = + (delays.map_or_else(|| 1.0, |f| *f.get(frame).unwrap_or(&1.0)) * 10.0) as u16; + // the disposal method by default is "keep the previous frame under the alpha mask" + // wtf + gif_frame.dispose = DisposalMethod::Background; + encoder.write_frame(&gif_frame).unwrap(); + // Clear the canvas. + canvas.clear(); + } + + Ok(()) + } + + /// Renders the whole file to a png, discarding all but the first frame of animations + fn render_to_png(&self, icon_state: &State, target: W) -> io::Result<()> { + let mut canvas = self.get_canvas(icon_state.dirs); + + self.render_dirs(icon_state, &mut canvas, 0); + + canvas.to_write(target)?; + // Always remember to clear the canvas for the next guy! + canvas.clear(); + Ok(()) + } +} diff --git a/crates/dmm-tools/src/dmm.rs b/crates/dmm-tools/src/dmm.rs index b69c0cf1..39a42c4a 100644 --- a/crates/dmm-tools/src/dmm.rs +++ b/crates/dmm-tools/src/dmm.rs @@ -1,16 +1,16 @@ use std::collections::BTreeMap; -use std::path::Path; +use std::fmt; use std::fs::File; use std::io; -use std::fmt; +use std::path::Path; -use ndarray::{self, Array3, Axis}; +use foldhash::fast::RandomState; use indexmap::IndexMap; -use ahash::RandomState; +use ndarray::{self, Array3, Axis}; -use dm::DMError; -use dm::constants::Constant; use crate::dmi::Dir; +use dm::constants::Constant; +use dm::DMError; mod read; mod save_tgm; @@ -18,7 +18,7 @@ mod save_tgm; const MAX_KEY_LENGTH: u8 = 3; /// BYOND is currently limited to 65534 keys. -/// https://secure.byond.com/forum/?post=2340796#comment23770802 +/// https://www.byond.com/forum/?post=2340796#comment23770802 type KeyType = u16; /// An opaque map key. @@ -28,7 +28,7 @@ pub struct Key(KeyType); /// An XY coordinate pair in the BYOND coordinate system. /// /// The lower-left corner is `{ x: 1, y: 1 }`. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Coord2 { pub x: i32, pub y: i32, @@ -42,17 +42,34 @@ impl Coord2 { #[inline] pub fn z(self, z: i32) -> Coord3 { - Coord3 { x: self.x, y: self.y, z } + Coord3 { + x: self.x, + y: self.y, + z, + } } fn to_raw(self, (dim_y, dim_x): (usize, usize)) -> (usize, usize) { - assert!(self.x >= 1 && self.x <= dim_x as i32, "x={} not in [1, {}]", self.x, dim_x); - assert!(self.y >= 1 && self.y <= dim_y as i32, "y={} not in [1, {}]", self.y, dim_y); + assert!( + self.x >= 1 && self.x <= dim_x as i32, + "x={} not in [1, {}]", + self.x, + dim_x + ); + assert!( + self.y >= 1 && self.y <= dim_y as i32, + "y={} not in [1, {}]", + self.y, + dim_y + ); (dim_y - self.y as usize, self.x as usize - 1) } fn from_raw((y, x): (usize, usize), (dim_y, _dim_x): (usize, usize)) -> Coord2 { - Coord2 { x: x as i32 + 1, y: (dim_y - y) as i32 } + Coord2 { + x: x as i32 + 1, + y: (dim_y - y) as i32, + } } } @@ -61,7 +78,10 @@ impl std::ops::Add for Coord2 { fn add(self, rhs: Dir) -> Coord2 { let (x, y) = rhs.offset(); - Coord2 { x: self.x + x, y: self.y + y } + Coord2 { + x: self.x + x, + y: self.y + y, + } } } @@ -71,7 +91,7 @@ impl std::ops::Add for Coord2 { /// /// Note that BYOND by default considers "UP" to be Z+1, but this does not /// necessarily apply to a given game's logic. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Coord3 { pub x: i32, pub y: i32, @@ -86,19 +106,48 @@ impl Coord3 { #[inline] pub fn xy(self) -> Coord2 { - Coord2 { x: self.x, y: self.y } + Coord2 { + x: self.x, + y: self.y, + } } fn to_raw(self, (dim_z, dim_y, dim_x): (usize, usize, usize)) -> (usize, usize, usize) { - assert!(self.x >= 1 && self.x <= dim_x as i32, "x={} not in [1, {}]", self.x, dim_x); - assert!(self.y >= 1 && self.y <= dim_y as i32, "y={} not in [1, {}]", self.y, dim_y); - assert!(self.z >= 1 && self.z <= dim_z as i32, "y={} not in [1, {}]", self.z, dim_z); - (self.z as usize - 1, dim_y - self.y as usize, self.x as usize - 1) + assert!( + self.x >= 1 && self.x <= dim_x as i32, + "x={} not in [1, {}]", + self.x, + dim_x + ); + assert!( + self.y >= 1 && self.y <= dim_y as i32, + "y={} not in [1, {}]", + self.y, + dim_y + ); + assert!( + self.z >= 1 && self.z <= dim_z as i32, + "y={} not in [1, {}]", + self.z, + dim_z + ); + ( + self.z as usize - 1, + dim_y - self.y as usize, + self.x as usize - 1, + ) } #[allow(dead_code)] - fn from_raw((z, y, x): (usize, usize, usize), (_dim_z, dim_y, _dim_x): (usize, usize, usize)) -> Coord3 { - Coord3 { x: x as i32 + 1, y: (dim_y - y) as i32, z: z as i32 + 1 } + fn from_raw( + (z, y, x): (usize, usize, usize), + (_dim_z, dim_y, _dim_x): (usize, usize, usize), + ) -> Coord3 { + Coord3 { + x: x as i32 + 1, + y: (dim_y - y) as i32, + z: z as i32 + 1, + } } } @@ -118,32 +167,43 @@ pub struct ZLevel<'a> { pub grid: ndarray::ArrayView<'a, Key, ndarray::Dim<[usize; 2]>>, } -// TODO: port to ast::Prefab -#[derive(Debug, Default, Eq, PartialEq, Clone)] +#[derive(Debug, Default, Clone)] pub struct Prefab { pub path: String, // insertion order, sort of most of the time alphabetical but not quite pub vars: IndexMap, } +impl PartialEq for Prefab { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && self.vars == other.vars + } +} + +impl Eq for Prefab {} + impl std::hash::Hash for Prefab { fn hash(&self, state: &mut H) { self.path.hash(state); - self.vars.keys().for_each(|key| {key.hash(state)}); + let mut items: Vec<_> = self.vars.iter().collect(); + items.sort_by_key(|&(k, _)| k); + for kvp in items { + kvp.hash(state); + } } } impl Map { pub fn new(x: usize, y: usize, z: usize, turf: String, area: String) -> Map { - assert!(x > 0 && y > 0 && z > 0, "({}, {}, {})", x, y, z); + assert!(x > 0 && y > 0 && z > 0, "({x}, {y}, {z})"); let mut dictionary = BTreeMap::new(); - dictionary.insert(Key(0), vec![ - Prefab::from_path(turf), - Prefab::from_path(area), - ]); + dictionary.insert( + Key(0), + vec![Prefab::from_path(turf), Prefab::from_path(area)], + ); - let grid = Array3::default((z, y, x)); // default = 0 + let grid = Array3::default((z, y, x)); // default = 0 Map { key_length: 1, @@ -170,9 +230,12 @@ impl Map { Ok(map) } + pub fn to_writer(&self, writer: &mut impl std::io::Write) -> io::Result<()> { + save_tgm::save_tgm(self, writer) + } + pub fn to_file(&self, path: &Path) -> io::Result<()> { - // DMM saver later - save_tgm::save_tgm(self, File::create(path)?) + self.to_writer(&mut File::create(path)?) } pub fn key_length(&self) -> u8 { @@ -180,12 +243,15 @@ impl Map { } pub fn adjust_key_length(&mut self) { - if self.dictionary.len() > 2704 { - self.key_length = 3; - } else if self.dictionary.len() > 52 { - self.key_length = 2; - } else { - self.key_length = 1; + if let Some(max_key) = self.dictionary.keys().max() { + let max_key = max_key.0; + if max_key >= 2704 { + self.key_length = 3; + } else if max_key >= 52 { + self.key_length = 2; + } else { + self.key_length = 1; + } } } @@ -201,12 +267,17 @@ impl Map { } #[inline] - pub fn z_level(&self, z: usize) -> ZLevel { - ZLevel { grid: self.grid.index_axis(Axis(0), z) } + pub fn z_level(&self, z: usize) -> ZLevel<'_> { + ZLevel { + grid: self.grid.index_axis(Axis(0), z), + } } - pub fn iter_levels<'a>(&'a self) -> impl Iterator)> + 'a { - self.grid.axis_iter(Axis(0)).enumerate().map(|(i, grid)| (i as i32 + 1, ZLevel { grid })) + pub fn iter_levels(&self) -> impl Iterator)> + '_ { + self.grid + .axis_iter(Axis(0)) + .enumerate() + .map(|(i, grid)| (i as i32 + 1, ZLevel { grid })) } #[inline] @@ -226,9 +297,11 @@ impl std::ops::Index for Map { impl<'a> ZLevel<'a> { /// Iterate over the z-level in row-major order starting at the top-left. - pub fn iter_top_down<'b>(&'b self) -> impl Iterator + 'b { + pub fn iter_top_down(&self) -> impl Iterator + '_ { let dim = self.grid.dim(); - self.grid.indexed_iter().map(move |(c, k)| (Coord2::from_raw(c, dim), *k)) + self.grid + .indexed_iter() + .map(move |(c, k)| (Coord2::from_raw(c, dim), *k)) } } @@ -264,7 +337,7 @@ impl fmt::Display for Prefab { if f.alternate() { f.write_str("\n ")?; } - write!(f, "{} = {}", k, v)?; + write!(f, "{k} = {v}")?; } if f.alternate() { f.write_str("\n")?; @@ -289,7 +362,7 @@ impl fmt::Display for Coord3 { impl Key { pub fn invalid() -> Key { - Key(KeyType::max_value()) + Key(KeyType::MAX) } pub fn next(self) -> Key { @@ -322,9 +395,9 @@ impl fmt::Display for FormatKey { const BASE_52: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; fn base_52_reverse(ch: u8) -> Result { - if ch >= b'a' && ch <= b'z' { + if ch.is_ascii_lowercase() { Ok(ch as KeyType - b'a' as KeyType) - } else if ch >= b'A' && ch <= b'Z' { + } else if ch.is_ascii_uppercase() { Ok(26 + ch as KeyType - b'A' as KeyType) } else { Err(format!("Not a base-52 character: {:?}", ch as char)) @@ -332,8 +405,11 @@ fn base_52_reverse(ch: u8) -> Result { } fn advance_key(current: KeyType, next_digit: KeyType) -> Result { - current.checked_mul(52).and_then(|b| b.checked_add(next_digit)).ok_or_else(|| { - // https://secure.byond.com/forum/?post=2340796#comment23770802 - "Key overflow, max is 'ymo'" - }) + current + .checked_mul(52) + .and_then(|b| b.checked_add(next_digit)) + .ok_or({ + // https://www.byond.com/forum/?post=2340796#comment23770802 + "Key overflow, max is 'ymo'" + }) } diff --git a/crates/dmm-tools/src/dmm/read.rs b/crates/dmm-tools/src/dmm/read.rs index 771c7c51..1885a701 100644 --- a/crates/dmm-tools/src/dmm/read.rs +++ b/crates/dmm-tools/src/dmm/read.rs @@ -1,18 +1,14 @@ //! Map parser, supporting standard DMM or TGM-format files. -use std::collections::BTreeMap; use std::cmp::max; +use std::collections::BTreeMap; +use std::mem::take; use ndarray::Array3; +use dm::lexer::{from_utf8_or_latin1, LocationTracker}; use dm::{DMError, Location}; -use dm::lexer::{LocationTracker, from_utf8_or_latin1}; -use super::{Map, Key, KeyType, Prefab}; - -#[inline] -fn take(t: &mut T) -> T { - std::mem::replace(t, T::default()) -} +use super::{Key, KeyType, Map, Prefab}; pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { let file_id = Default::default(); @@ -37,7 +33,38 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { let mut escaping = false; let mut skip_whitespace = false; + let mut curr_key_start_location = Location::default(); + + let mut curr_datum_start_location = Location::default(); + macro_rules! set_curr_datum_start_location { + () => { + if curr_datum.is_empty() { + curr_datum_start_location = chars.location(); + } + }; + } + + macro_rules! insert_current_var { + () => { + curr_prefab.vars.insert( + from_utf8_or_latin1(take(&mut curr_var)), + dm::constants::evaluate_str(curr_datum_start_location, &take(&mut curr_datum)) + .map_err(|e| { + e.with_note( + curr_key_start_location, + format!( + "within key: \"{}\"", + super::FormatKey(curr_key_length, super::Key(curr_key)) + ), + ) + })?, + ); + }; + } + while let Some(ch) = chars.next() { + // Readability, simple elif chain isn't duplicate code + #[allow(clippy::if_same_then_else)] if ch == b'\n' || ch == b'\r' { in_comment_line = false; comment_trigger = false; @@ -63,18 +90,23 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { if in_varedit_block { if in_quote_block { if ch == b'\\' { + set_curr_datum_start_location!(); curr_datum.push(ch); escaping = true; } else if escaping { + set_curr_datum_start_location!(); curr_datum.push(ch); escaping = false; } else if ch == b'"' { + set_curr_datum_start_location!(); curr_datum.push(ch); in_quote_block = false; } else { + set_curr_datum_start_location!(); curr_datum.push(ch); } - } else { // in_quote_block + } else { + // in_quote_block if skip_whitespace && ch == b' ' { skip_whitespace = false; continue; @@ -82,6 +114,7 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { skip_whitespace = false; if ch == b'"' { + set_curr_datum_start_location!(); curr_datum.push(ch); in_quote_block = true; } else if ch == b'=' && curr_var.is_empty() { @@ -93,20 +126,15 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { curr_var.truncate(length); skip_whitespace = true; } else if ch == b';' { - curr_prefab.vars.insert( - from_utf8_or_latin1(take(&mut curr_var)), - dm::constants::evaluate_str(chars.location(), &take(&mut curr_datum))?, - ); + insert_current_var!(); skip_whitespace = true; } else if ch == b'}' { if !curr_var.is_empty() { - curr_prefab.vars.insert( - from_utf8_or_latin1(take(&mut curr_var)), - dm::constants::evaluate_str(chars.location(), &take(&mut curr_datum))?, - ); + insert_current_var!(); } in_varedit_block = false; } else { + set_curr_datum_start_location!(); curr_datum.push(ch); } } @@ -130,6 +158,7 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { in_data_block = false; after_data_block = true; } else { + set_curr_datum_start_location!(); curr_datum.push(ch); } } else if in_key_block { @@ -138,6 +167,9 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { assert!(map.key_length == 0 || map.key_length == curr_key_length); map.key_length = curr_key_length; } else { + if curr_key == 0 { + curr_key_start_location = chars.location(); + } curr_key = advance_key(chars.location(), curr_key, ch)?; curr_key_length += 1; } @@ -188,7 +220,10 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { max_y = max(max_y, curr_y); reading_coord = Coord::Z; } else { - return Err(DMError::new(chars.location(), "Incorrect number of coordinates")); + return Err(DMError::new( + chars.location(), + "Incorrect number of coordinates", + )); } } else if ch == b')' { assert_eq!(reading_coord, Coord::Z); @@ -199,7 +234,12 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { } else { match (ch as char).to_digit(10) { Some(x) => curr_num = 10 * curr_num + x as usize, - None => return Err(DMError::new(chars.location(), format!("bad digit {:?} in map coordinate", ch))), + None => { + return Err(DMError::new( + chars.location(), + format!("bad digit {ch:?} in map coordinate"), + )) + }, } } } else if in_map_string { @@ -223,9 +263,10 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { let key = take(&mut curr_key); curr_key_length = 0; if grid.insert((curr_x, curr_y, curr_z), Key(key)).is_some() { - return Err(DMError::new(chars.location(), format!( - "multiple entries for ({}, {}, {})", - curr_x, curr_y, curr_z))) + return Err(DMError::new( + chars.location(), + format!("multiple entries for ({curr_x}, {curr_y}, {curr_z})"), + )); } max_x = max(max_x, curr_x); curr_x += 1; @@ -244,9 +285,10 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> { if let Some(&tile) = grid.get(&(x + 1, y + 1, z + 1)) { tile } else { - result = Err(DMError::new(chars.location(), format!( - "no value for tile ({}, {}, {})", - x + 1, y + 1, z + 1))); + result = Err(DMError::new( + chars.location(), + format!("no value for tile ({}, {}, {})", x + 1, y + 1, z + 1), + )); Key(0) } }); @@ -260,6 +302,6 @@ fn advance_key(loc: Location, curr_key: KeyType, ch: u8) -> Result match super::advance_key(curr_key, single) { Err(err) => Err(DMError::new(loc, err)), Ok(key) => Ok(key), - } + }, } } diff --git a/crates/dmm-tools/src/dmm/save_tgm.rs b/crates/dmm-tools/src/dmm/save_tgm.rs index 46677048..a43b5fce 100644 --- a/crates/dmm-tools/src/dmm/save_tgm.rs +++ b/crates/dmm-tools/src/dmm/save_tgm.rs @@ -1,26 +1,28 @@ //! TGM map writer. -use std::fs::File; -use std::io::{self, Write, BufWriter}; +use std::io::{self, BufWriter, Write}; use ndarray::Axis; use super::Map; -const TGM_HEADER: &str = "//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE"; +const TGM_HEADER: &str = + "//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE"; -pub fn save_tgm(map: &Map, f: File) -> io::Result<()> { - let mut f = BufWriter::new(f); - write!(f, "{}\n", TGM_HEADER)?; +// Note: writeln! currently (2022-04-30) writes the \n character alone on all platforms +// If that changes, this will break. +pub fn save_tgm(map: &Map, w: &mut impl Write) -> io::Result<()> { + let mut f = BufWriter::new(w); + writeln!(f, "{TGM_HEADER}")?; // dictionary for (&key, prefabs) in map.dictionary.iter() { - write!(f, "\"{}\" = (\n", map.format_key(key))?; + writeln!(f, "\"{}\" = (", map.format_key(key))?; for (i, fab) in prefabs.iter().enumerate() { write!(f, "{}", fab.path)?; if !fab.vars.is_empty() { write!(f, "{{")?; for (i, (var, value)) in fab.vars.iter().enumerate() { - write!(f, "\n\t{} = {}", var, value)?; + write!(f, "\n\t{var} = {value}")?; if i + 1 != fab.vars.len() { write!(f, ";")?; } @@ -28,21 +30,21 @@ pub fn save_tgm(map: &Map, f: File) -> io::Result<()> { write!(f, "\n\t}}")?; } if i + 1 != prefabs.len() { - write!(f, ",\n")?; + writeln!(f, ",")?; } } - write!(f, ")\n")?; + writeln!(f, ")")?; } // grid in Y-major for (z, z_grid) in map.grid.axis_iter(Axis(0)).enumerate() { - write!(f, "\n")?; + writeln!(f)?; for (x, x_col) in z_grid.axis_iter(Axis(1)).enumerate() { - write!(f, "({},1,{}) = {{\"\n", x + 1, z + 1)?; + writeln!(f, "({},1,{}) = {{\"", x + 1, z + 1)?; for &elem in x_col.iter() { - write!(f, "{}\n", map.format_key(elem))?; + writeln!(f, "{}", map.format_key(elem))?; } - write!(f, "\"}}\n")?; + writeln!(f, "\"}}")?; } } diff --git a/crates/dmm-tools/src/icon_cache.rs b/crates/dmm-tools/src/icon_cache.rs index ec4fdde2..38f6e667 100644 --- a/crates/dmm-tools/src/icon_cache.rs +++ b/crates/dmm-tools/src/icon_cache.rs @@ -1,12 +1,13 @@ -use std::sync::{Arc, RwLock}; +use foldhash::fast::RandomState; +use std::collections::{hash_map, HashMap}; use std::path::{Path, PathBuf}; -use std::collections::{HashMap, hash_map}; +use std::sync::{Arc, RwLock}; use super::dmi::IconFile; #[derive(Default)] pub struct IconCache { - lock: RwLock>>>, + lock: RwLock>, RandomState>>, icons_root: Option, } @@ -15,12 +16,17 @@ impl IconCache { let map = self.lock.get_mut().unwrap(); (match map.entry(path.to_owned()) { hash_map::Entry::Occupied(entry) => entry.into_mut().as_mut(), - hash_map::Entry::Vacant(entry) => entry.insert( - match &self.icons_root { - Some(root) => load(&root.join(path)), - _ => load(path), - }.map(Arc::new)).as_mut(), - }).map(|x| &**x) + hash_map::Entry::Vacant(entry) => entry + .insert( + match &self.icons_root { + Some(root) => load(&root.join(path)), + _ => load(path), + } + .map(Arc::new), + ) + .as_mut(), + }) + .map(|x| &**x) } pub fn retrieve_shared(&self, path: &Path) -> Option> { @@ -31,9 +37,13 @@ impl IconCache { None => { let arc = match &self.icons_root { Some(root) => load(&root.join(path)), - None => load(&path), - }.map(Arc::new); - self.lock.write().unwrap().insert(path.to_owned(), arc.clone()); + None => load(path), + } + .map(Arc::new); + self.lock + .write() + .unwrap() + .insert(path.to_owned(), arc.clone()); arc }, } @@ -50,6 +60,6 @@ fn load(path: &Path) -> Option { Err(err) => { eprintln!("error loading icon: {}\n {}", path.display(), err); None - } + }, } } diff --git a/crates/dmm-tools/src/lib.rs b/crates/dmm-tools/src/lib.rs index 54ca27a6..319630f9 100644 --- a/crates/dmm-tools/src/lib.rs +++ b/crates/dmm-tools/src/lib.rs @@ -1,22 +1,25 @@ //! SS13 minimap generation tool -#![deny(unsafe_code)] // NB deny rather than forbid, ndarray macros use unsafe +#![deny(unsafe_code)] // NB deny rather than forbid, ndarray macros use unsafe extern crate dreammaker as dm; -#[cfg(feature="png")] extern crate png; -extern crate lodepng; extern crate inflate; +extern crate lodepng; +#[cfg(feature = "png")] +extern crate png; -#[macro_use] extern crate bytemuck; -extern crate rand; +#[macro_use] +extern crate bytemuck; extern crate bumpalo; +extern crate rand; -#[cfg(feature="gfx_core")] extern crate gfx_core; +#[cfg(feature = "gfx_core")] +extern crate gfx_core; +pub mod dmi; pub mod dmm; mod icon_cache; pub mod minimap; pub mod render_passes; -pub mod dmi; pub use icon_cache::IconCache; diff --git a/crates/dmm-tools/src/minimap.rs b/crates/dmm-tools/src/minimap.rs index e64e043b..880d5875 100644 --- a/crates/dmm-tools/src/minimap.rs +++ b/crates/dmm-tools/src/minimap.rs @@ -1,16 +1,16 @@ +use std::collections::BTreeMap; use std::sync::RwLock; -use std::collections::{HashSet, BTreeMap}; use ndarray::Axis; -use dm::objtree::*; -use dm::constants::Constant; -use crate::dmm::{Map, ZLevel, Prefab}; -use crate::dmi::{Dir, Image}; -use crate::render_passes::RenderPass; +use crate::dmi::{self, Dir, Image}; +use crate::dmm::{Map, Prefab, ZLevel}; use crate::icon_cache::IconCache; +use crate::render_passes::RenderPass; +use dm::constants::Constant; +use dm::objtree::*; -use ahash::RandomState; +use foldhash::HashSet; const TILE_SIZE: u32 = 32; @@ -25,10 +25,12 @@ pub struct Context<'a> { pub min: (usize, usize), pub max: (usize, usize), pub render_passes: &'a [Box], - pub errors: &'a RwLock>, + pub errors: &'a RwLock>, pub bump: &'a bumpalo::Bump, } +// This should eventually be faliable and not just shrug it's shoulders at errors and log them. +#[allow(clippy::result_unit_err)] pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result { let Context { objtree, @@ -48,7 +50,10 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result { // create atom arrays from the map dictionary let mut atoms = BTreeMap::new(); for (key, prefabs) in map.dictionary.iter() { - atoms.insert(key, get_atom_list(objtree, prefabs, render_passes, ctx.errors)); + atoms.insert( + key, + get_atom_list(objtree, prefabs, render_passes, ctx.errors), + ); } // loads atoms from the prefabs on the map and adds overlays and smoothing @@ -70,19 +75,19 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result { 'atom: for atom in atoms.get(e).expect("bad key").iter() { for pass in render_passes.iter() { // Note that late_filter is NOT called during smoothing lookups. - if !pass.late_filter(&atom, objtree) { + if !pass.late_filter(atom, objtree) { continue 'atom; } } let mut sprite = Sprite::from_vars(objtree, atom); for pass in render_passes { - pass.adjust_sprite(&atom, &mut sprite, objtree, bump); + pass.adjust_sprite(atom, &mut sprite, objtree, bump); } if sprite.icon.is_empty() { println!("no icon: {}", atom.type_.path); continue; } - let atom = Atom { sprite, .. *atom }; + let atom = Atom { sprite, ..*atom }; for pass in render_passes { pass.overlays(&atom, objtree, &mut underlays, &mut overlays, bump); @@ -90,11 +95,13 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result { // smoothing time let mut neighborhood = [&[][..]; 9]; - for (i, (dx, dy)) in [ + #[rustfmt::skip] + const GRID: [(i32, i32); 9] = [ (-1, 1), (0, 1), (1, 1), (-1, 0), (0, 0), (1, 0), (-1, -1), (0, -1), (1, -1), - ].iter().enumerate() { + ]; + for (i, (dx, dy)) in GRID.iter().enumerate() { let new_x = x as i32 + dx; let new_y = y as i32 - dy; let (dim_y, dim_x) = ctx.level.grid.dim(); @@ -107,7 +114,13 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result { let mut normal_appearance = true; for pass in render_passes { - if !pass.neighborhood_appearance(&atom, objtree, &neighborhood, &mut underlays, bump) { + if !pass.neighborhood_appearance( + &atom, + objtree, + &neighborhood, + &mut underlays, + bump, + ) { normal_appearance = false; } } @@ -123,37 +136,45 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result { drop(underlays); drop(overlays); - // sorts the atom list and renders them onto the output image - sprites.sort_by_key(|(_, s)| (s.plane, s.layer)); - - let mut map_image = Image::new_rgba(len_x as u32 * TILE_SIZE, len_y as u32 * TILE_SIZE); - 'sprite: for (loc, sprite) in sprites { + // Drop sprites rejected by any render pass. + sprites.retain(|(_, sprite)| { for pass in render_passes.iter() { - if !pass.sprite_filter(&sprite) { - continue 'sprite; + if !pass.sprite_filter(sprite) { + return false; } } + true + }); + // Sort the sprite list by depth. + sprites.sort_by_key(|(_, s)| (s.plane, s.layer)); + + // Composite the sorted sprites onto the output image. + let mut map_image = Image::new_rgba(len_x as u32 * TILE_SIZE, len_y as u32 * TILE_SIZE); + for ((x, y), sprite) in sprites { let icon_file = match icon_cache.retrieve_shared(sprite.icon.as_ref()) { Some(icon_file) => icon_file, None => continue, }; - if let Some(rect) = icon_file.rect_of(sprite.icon_state, sprite.dir) { + if let Some(rect) = icon_file.rect_of(&sprite.icon_state.into(), sprite.dir) { let pixel_x = sprite.ofs_x; let pixel_y = sprite.ofs_y + icon_file.metadata.height as i32; let loc = ( - ((loc.0 - ctx.min.0 as u32) * TILE_SIZE) as i32 + pixel_x, - ((loc.1 + 1 - min_y as u32) * TILE_SIZE) as i32 - pixel_y, + ((x - ctx.min.0 as u32) * TILE_SIZE) as i32 + pixel_x, + ((y + 1 - min_y as u32) * TILE_SIZE) as i32 - pixel_y, ); if let Some((loc, rect)) = clip((map_image.width, map_image.height), loc, rect) { map_image.composite(&icon_file.image, loc, rect, sprite.color); } } else { - let key = format!("bad icon: {:?}, state: {:?}", sprite.icon, sprite.icon_state); + let key = format!( + "bad icon: {:?}, state: {:?}", + sprite.icon, sprite.icon_state + ); if !ctx.errors.read().unwrap().contains(&key) { - eprintln!("{}", key); + eprintln!("{key}"); ctx.errors.write().unwrap().insert(key); } } @@ -163,34 +184,28 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result { } // OOB handling -fn clip(bounds: (u32, u32), mut loc: (i32, i32), mut rect: (u32, u32, u32, u32)) -> Option<((u32, u32), (u32, u32, u32, u32))> { +fn clip( + bounds: dmi::Coordinate, + mut loc: (i32, i32), + mut rect: dmi::Rect, +) -> Option<(dmi::Coordinate, dmi::Rect)> { if loc.0 < 0 { rect.0 += (-loc.0) as u32; - match rect.2.checked_sub((-loc.0) as u32) { - Some(s) => rect.2 = s, - None => return None, // out of the viewport - } + rect.2 = rect.2.checked_sub((-loc.0) as u32)?; loc.0 = 0; } - while loc.0 + rect.2 as i32 > bounds.0 as i32 { - rect.2 -= 1; - if rect.2 == 0 { - return None; - } + let overhang = loc.0 + rect.2 as i32 - bounds.0 as i32; + if overhang > 0 { + rect.2 = rect.2.checked_sub(overhang as u32)?; } if loc.1 < 0 { rect.1 += (-loc.1) as u32; - match rect.3.checked_sub((-loc.1) as u32) { - Some(s) => rect.3 = s, - None => return None, // out of the viewport - } + rect.3 = rect.3.checked_sub((-loc.1) as u32)?; loc.1 = 0; } - while loc.1 + rect.3 as i32 > bounds.1 as i32 { - rect.3 -= 1; - if rect.3 == 0 { - return None; - } + let overhang = loc.1 + rect.3 as i32 - bounds.1 as i32; + if overhang > 0 { + rect.3 = rect.3.checked_sub(overhang as u32)?; } Some(((loc.0 as u32, loc.1 as u32), rect)) } @@ -199,7 +214,7 @@ fn get_atom_list<'a>( objtree: &'a ObjectTree, prefabs: &'a [Prefab], render_passes: &[Box], - errors: &RwLock>, + errors: &RwLock>, ) -> Vec> { let mut result = Vec::new(); @@ -216,11 +231,11 @@ fn get_atom_list<'a>( None => { let key = format!("bad path: {}", fab.path); if !errors.read().unwrap().contains(&key) { - println!("{}", key); + println!("{key}"); errors.write().unwrap().insert(key); } continue; - } + }, }; for pass in render_passes { @@ -261,14 +276,14 @@ impl<'a> Atom<'a> { } pub fn istype(&self, parent: &str) -> bool { - subpath(&self.type_.path, parent) + ispath(&self.type_.path, parent) } } impl<'a> From<&'a Type> for Atom<'a> { fn from(type_: &'a Type) -> Self { Atom { - type_: type_, + type_, prefab: None, sprite: Sprite::default(), } @@ -318,7 +333,8 @@ pub trait GetVar<'a> { fn get_path(&self) -> &str; fn get_var(&self, key: &str, objtree: &'a ObjectTree) -> &'a Constant { - self.get_var_inner(key, objtree).unwrap_or(Constant::null()) + self.get_var_inner(key, objtree) + .unwrap_or_else(Constant::null) } fn get_var_notnull(&self, key: &str, objtree: &'a ObjectTree) -> Option<&'a Constant> { @@ -337,7 +353,7 @@ impl<'a> GetVar<'a> for Atom<'a> { } fn get_var_inner(&self, key: &str, objtree: &'a ObjectTree) -> Option<&'a Constant> { - if let Some(ref prefab) = self.prefab { + if let Some(prefab) = self.prefab { if let Some(v) = prefab.get(key) { return Some(v); } @@ -345,7 +361,7 @@ impl<'a> GetVar<'a> for Atom<'a> { let mut current = Some(self.type_); while let Some(t) = current.take() { if let Some(v) = t.vars.get(key) { - return Some(v.value.constant.as_ref().unwrap_or(Constant::null())); + return Some(v.value.constant.as_ref().unwrap_or_else(Constant::null)); } current = objtree.parent_of(t); } @@ -365,7 +381,7 @@ impl<'a> GetVar<'a> for &'a Prefab { let mut current = objtree.find(&self.path); while let Some(t) = current.take() { if let Some(v) = t.get().vars.get(key) { - return Some(v.value.constant.as_ref().unwrap_or(Constant::null())); + return Some(v.value.constant.as_ref().unwrap_or_else(Constant::null)); } current = t.parent_type(); } @@ -382,7 +398,7 @@ impl<'a> GetVar<'a> for &'a Type { let mut current = Some(*self); while let Some(t) = current.take() { if let Some(v) = t.vars.get(key) { - return Some(v.value.constant.as_ref().unwrap_or(Constant::null())); + return Some(v.value.constant.as_ref().unwrap_or_else(Constant::null)); } current = objtree.parent_of(t); } @@ -399,7 +415,7 @@ impl<'a> GetVar<'a> for TypeRef<'a> { let mut current = Some(*self); while let Some(t) = current.take() { if let Some(v) = t.get().vars.get(key) { - return Some(v.value.constant.as_ref().unwrap_or(Constant::null())); + return Some(v.value.constant.as_ref().unwrap_or_else(Constant::null)); } current = t.parent_type(); } @@ -410,46 +426,6 @@ impl<'a> GetVar<'a> for TypeRef<'a> { // ---------------------------------------------------------------------------- // Renderer-agnostic sprite structure -/// Information about when a sprite should be shown or hidden. -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash)] -pub struct Category { - raw: u32, -} - -impl Category { - const AREA: Category = Category { raw: 1 }; - const TURF: Category = Category { raw: 2 }; - const OBJ: Category = Category { raw: 3 }; - const MOB: Category = Category { raw: 4 }; - - /// - pub fn from_path(path: &str) -> Category { - if path.starts_with("/area") { - Category::AREA - } else if path.starts_with("/turf") { - Category::TURF - } else if path.starts_with("/obj") { - Category::OBJ - } else if path.starts_with("/mob") { - Category::MOB - } else { - Category { raw: 0 } - } - } - - /// Encode this category for FFI representation. - pub fn matches_basic_layers(self, visible: &[bool]) -> bool { - visible.get(self.raw as usize).copied().unwrap_or(false) - } -} - -#[cfg(feature="gfx_core")] -impl gfx_core::shade::BaseTyped for Category { - fn get_base_type() -> gfx_core::shade::BaseType { - u32::get_base_type() - } -} - /// A guaranteed sortable representation of a `layer` float. #[derive(Default, Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct Layer { @@ -466,17 +442,23 @@ impl From for Layer { impl From for Layer { fn from(whole: i32) -> Layer { use std::convert::TryFrom; - Layer { whole: i16::try_from(whole).expect("layer out of range"), frac: 0 } + Layer { + whole: i16::try_from(whole).expect("layer out of range"), + frac: 0, + } } } impl From for Layer { fn from(f: f32) -> Layer { - Layer { whole: f.floor() as i16, frac: ((f.fract() + 1.).fract() * 65536.) as u16 } + Layer { + whole: f.floor() as i16, + frac: ((f.fract() + 1.).fract() * 65536.) as u16, + } } } -#[cfg(feature="gfx_core")] +#[cfg(feature = "gfx_core")] impl gfx_core::shade::BaseTyped for Layer { fn get_base_type() -> gfx_core::shade::BaseType { i32::get_base_type() @@ -489,18 +471,15 @@ impl gfx_core::shade::BaseTyped for Layer { /// overlays. #[derive(Debug, Clone)] pub struct Sprite<'s> { - // filtering - pub category: Category, - // visual appearance pub icon: &'s str, pub icon_state: &'s str, pub dir: Dir, - pub color: [u8; 4], // [r, g, b, a] + pub color: [u8; 4], // [r, g, b, a] // position - pub ofs_x: i32, // pixel_x + pixel_w + step_x - pub ofs_y: i32, // pixel_y + pixel_z + step_y + pub ofs_x: i32, // pixel_x + pixel_w + step_x + pub ofs_y: i32, // pixel_y + pixel_z + step_y // sorting pub plane: i32, @@ -517,10 +496,13 @@ impl<'s> Sprite<'s> { let step_y = vars.get_var("step_y", objtree).to_int().unwrap_or(0); Sprite { - category: Category::from_path(vars.get_path()), icon: vars.get_var("icon", objtree).as_path_str().unwrap_or(""), icon_state: vars.get_var("icon_state", objtree).as_str().unwrap_or(""), - dir: vars.get_var("dir", objtree).to_int().and_then(Dir::from_int).unwrap_or(Dir::default()), + dir: vars + .get_var("dir", objtree) + .to_int() + .and_then(Dir::from_int) + .unwrap_or_default(), color: color_of(objtree, vars), ofs_x: pixel_x + pixel_w + step_x, ofs_y: pixel_y + pixel_z + step_y, @@ -533,7 +515,6 @@ impl<'s> Sprite<'s> { impl<'s> Default for Sprite<'s> { fn default() -> Self { Sprite { - category: Category::default(), icon: "", icon_state: "", dir: Dir::default(), @@ -552,7 +533,7 @@ fn plane_of<'s, T: GetVar<'s> + ?Sized>(objtree: &'s ObjectTree, atom: &T) -> i3 other => { eprintln!("not a plane: {:?} on {:?}", other, atom.get_path()); 0 - } + }, } } @@ -562,25 +543,27 @@ pub(crate) fn layer_of<'s, T: GetVar<'s> + ?Sized>(objtree: &'s ObjectTree, atom other => { eprintln!("not a layer: {:?} on {:?}", other, atom.get_path()); Layer::from(2) - } + }, } } pub fn color_of<'s, T: GetVar<'s> + ?Sized>(objtree: &'s ObjectTree, atom: &T) -> [u8; 4] { let alpha = match atom.get_var("alpha", objtree) { - &Constant::Float(i) if i >= 0. && i <= 255. => i as u8, + &Constant::Float(i) if (0. ..=255.).contains(&i) => i as u8, _ => 255, }; - match atom.get_var("color", objtree) { - &Constant::String(ref color) if color.starts_with("#") => { + match *atom.get_var("color", objtree) { + Constant::String(ref color) if color.starts_with('#') => { let mut sum = 0; for ch in color[1..color.len()].chars() { sum = 16 * sum + ch.to_digit(16).unwrap_or(0); } - if color.len() == 7 { // #rrggbb + if color.len() == 7 { + // #rrggbb [(sum >> 16) as u8, (sum >> 8) as u8, sum as u8, alpha] - } else if color.len() == 4 { // #rgb + } else if color.len() == 4 { + // #rgb [ (0x11 * ((sum >> 8) & 0xf)) as u8, (0x11 * ((sum >> 4) & 0xf)) as u8, @@ -588,13 +571,13 @@ pub fn color_of<'s, T: GetVar<'s> + ?Sized>(objtree: &'s ObjectTree, atom: &T) - alpha, ] } else { - [255, 255, 255, alpha] // invalid + [255, 255, 255, alpha] // invalid } - } - &Constant::String(ref color) => match html_color(color) { + }, + Constant::String(ref color) => match html_color(color) { Some([r, g, b]) => [r, g, b, alpha], None => [255, 255, 255, alpha], - } + }, // TODO: color matrix support? _ => [255, 255, 255, alpha], } diff --git a/crates/dmm-tools/src/render_passes/icon_smoothing.rs b/crates/dmm-tools/src/render_passes/icon_smoothing.rs index c0d2b51a..3c92ae23 100644 --- a/crates/dmm-tools/src/render_passes/icon_smoothing.rs +++ b/crates/dmm-tools/src/render_passes/icon_smoothing.rs @@ -1,9 +1,9 @@ //! Port of icon smoothing subsystem. -use dm::objtree::ObjectTree; -use dm::constants::Constant; use crate::dmi::Dir; -use crate::minimap::{Sprite, Atom, GetVar, Neighborhood}; +use crate::minimap::{Atom, GetVar, Neighborhood, Sprite}; +use dm::constants::Constant; +use dm::objtree::ObjectTree; use super::RenderPass; @@ -17,10 +17,10 @@ const N_NORTHWEST: i32 = 512; const N_SOUTHEAST: i32 = 64; const N_SOUTHWEST: i32 = 1024; -const SMOOTH_TRUE: i32 = 1; // smooth with exact specified types or just itself -const SMOOTH_MORE: i32 = 2; // smooth with all subtypes thereof -const SMOOTH_DIAGONAL: i32 = 4; // smooth diagonally -const SMOOTH_BORDER: i32 = 8; // smooth with the borders of the map +const SMOOTH_TRUE: i32 = 1; // smooth with exact specified types or just itself +const SMOOTH_MORE: i32 = 2; // smooth with all subtypes thereof +const SMOOTH_DIAGONAL: i32 = 4; // smooth diagonally +const SMOOTH_BORDER: i32 = 8; // smooth with the borders of the map pub struct IconSmoothing { pub mask: i32, @@ -33,7 +33,8 @@ impl Default for IconSmoothing { } impl RenderPass for IconSmoothing { - fn adjust_sprite<'a>(&self, + fn adjust_sprite<'a>( + &self, atom: &Atom<'a>, sprite: &mut Sprite<'a>, _objtree: &'a ObjectTree, @@ -45,7 +46,8 @@ impl RenderPass for IconSmoothing { } } - fn neighborhood_appearance<'a>(&self, + fn neighborhood_appearance<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, neighborhood: &Neighborhood<'a, '_>, @@ -67,7 +69,12 @@ impl RenderPass for IconSmoothing { } } -fn calculate_adjacencies(objtree: &ObjectTree, neighborhood: &Neighborhood, atom: &Atom, smooth_flags: i32) -> i32 { +fn calculate_adjacencies( + objtree: &ObjectTree, + neighborhood: &Neighborhood, + atom: &Atom, + smooth_flags: i32, +) -> i32 { // TODO: anchored check let mut adjacencies = 0; @@ -103,29 +110,37 @@ fn calculate_adjacencies(objtree: &ObjectTree, neighborhood: &Neighborhood, atom adjacencies } -fn find_type_in_direction(objtree: &ObjectTree, adjacency: &Neighborhood, source: &Atom, direction: Dir, smooth_flags: i32) -> bool { +fn find_type_in_direction( + objtree: &ObjectTree, + adjacency: &Neighborhood, + source: &Atom, + direction: Dir, + smooth_flags: i32, +) -> bool { let atom_list = adjacency.offset(direction); if atom_list.is_empty() { return smooth_flags & SMOOTH_BORDER != 0; } match source.get_var("canSmoothWith", objtree) { - &Constant::List(ref elements) => if smooth_flags & SMOOTH_MORE != 0 { - // smooth with canSmoothWith + subtypes - for atom in atom_list { - let mut path = atom.get_path(); - while !path.is_empty() { - if smoothlist_contains(elements, path) { + Constant::List(elements) => { + if smooth_flags & SMOOTH_MORE != 0 { + // smooth with canSmoothWith + subtypes + for atom in atom_list { + let mut path = atom.get_path(); + while !path.is_empty() { + if smoothlist_contains(elements, path) { + return true; + } + path = &path[..path.rfind('/').unwrap()]; + } + } + } else { + // smooth only with exact types in canSmoothWith + for atom in atom_list { + if smoothlist_contains(elements, atom.get_path()) { return true; } - path = &path[..path.rfind("/").unwrap()]; - } - } - } else { - // smooth only with exact types in canSmoothWith - for atom in atom_list { - if smoothlist_contains(elements, atom.get_path()) { - return true; } } }, @@ -142,16 +157,21 @@ fn find_type_in_direction(objtree: &ObjectTree, adjacency: &Neighborhood, source } fn smoothlist_contains(list: &[(Constant, Option)], desired: &str) -> bool { - for &(ref key, _) in list { - // TODO: be more specific than to_string - if key.to_string() == desired { + for (key, _) in list { + if key == desired { return true; } } false } -fn cardinal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bump: &'a bumpalo::Bump, source: &Atom<'a>, adjacencies: i32) { +fn cardinal_smooth<'a>( + output: &mut Vec>, + objtree: &'a ObjectTree, + bump: &'a bumpalo::Bump, + source: &Atom<'a>, + adjacencies: i32, +) { for &(what, f1, n1, f2, n2, f3) in &[ ("1", N_NORTH, "n", N_WEST, "w", N_NORTHWEST), ("2", N_NORTH, "n", N_EAST, "e", N_NORTHEAST), @@ -174,7 +194,7 @@ fn cardinal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu let mut sprite = Sprite { icon_state: name.into_bump_str(), - .. source.sprite + ..source.sprite }; if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() { sprite.icon = icon; @@ -183,7 +203,14 @@ fn cardinal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu } } -fn diagonal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bump: &'a bumpalo::Bump, neighborhood: &Neighborhood<'a, '_>, source: &Atom<'a>, adjacencies: i32) { +fn diagonal_smooth<'a>( + output: &mut Vec>, + objtree: &'a ObjectTree, + bump: &'a bumpalo::Bump, + neighborhood: &Neighborhood<'a, '_>, + source: &Atom<'a>, + adjacencies: i32, +) { let presets = if adjacencies == N_NORTH | N_WEST { ["d-se", "d-se-0"] } else if adjacencies == N_NORTH | N_EAST { @@ -212,7 +239,10 @@ fn diagonal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu .index(&Constant::string("space")) .is_some() { - output.push(Sprite::from_vars(objtree, &objtree.expect("/turf/open/space/basic"))); + output.push(Sprite::from_vars( + objtree, + &objtree.expect("/turf/open/space/basic"), + )); } else { let dir = reverse_ndir(adjacencies).flip(); let mut needs_plating = true; @@ -228,7 +258,10 @@ fn diagonal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu } } if needs_plating { - output.push(Sprite::from_vars(objtree, &objtree.expect("/turf/open/floor/plating"))); + output.push(Sprite::from_vars( + objtree, + &objtree.expect("/turf/open/floor/plating"), + )); } } } @@ -237,7 +270,7 @@ fn diagonal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu for &each in presets.iter() { let mut copy = Sprite { icon_state: each, - .. source.sprite + ..source.sprite }; if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() { copy.icon = icon; diff --git a/crates/dmm-tools/src/render_passes/icon_smoothing_2020.rs b/crates/dmm-tools/src/render_passes/icon_smoothing_2020.rs index 0c74fa30..aff702f3 100644 --- a/crates/dmm-tools/src/render_passes/icon_smoothing_2020.rs +++ b/crates/dmm-tools/src/render_passes/icon_smoothing_2020.rs @@ -4,10 +4,11 @@ //! followed by //! https://github.com/tgstation/tgstation/pull/53906 -use dm::objtree::ObjectTree; -use dm::constants::Constant; use crate::dmi::Dir; -use crate::minimap::{Sprite, Atom, GetVar, Neighborhood}; +use crate::minimap::{Atom, GetVar, Neighborhood, Sprite}; +use dm::constants::Constant; +use dm::objtree::ObjectTree; +use foldhash::HashSet; use super::RenderPass; @@ -40,7 +41,8 @@ impl Default for IconSmoothing { } impl RenderPass for IconSmoothing { - fn adjust_sprite<'a>(&self, + fn adjust_sprite<'a>( + &self, atom: &Atom<'a>, sprite: &mut Sprite<'a>, _objtree: &'a ObjectTree, @@ -52,14 +54,19 @@ impl RenderPass for IconSmoothing { } } - fn neighborhood_appearance<'a>(&self, + fn neighborhood_appearance<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, neighborhood: &Neighborhood<'a, '_>, output: &mut Vec>, bump: &'a bumpalo::Bump, ) -> bool { - let smooth_flags = self.mask & atom.get_var("smoothing_flags", objtree).to_int().unwrap_or(0); + let smooth_flags = self.mask + & atom + .get_var("smoothing_flags", objtree) + .to_int() + .unwrap_or(0); if smooth_flags & SMOOTH_CORNERS != 0 { let adjacencies = calculate_adjacencies(objtree, neighborhood, atom, smooth_flags); if smooth_flags & SMOOTH_DIAGONAL_CORNERS != 0 { @@ -70,7 +77,15 @@ impl RenderPass for IconSmoothing { false } else if smooth_flags & SMOOTH_BITMASK != 0 { let adjacencies = calculate_adjacencies(objtree, neighborhood, atom, smooth_flags); - bitmask_smooth(output, objtree, bump, neighborhood, atom, adjacencies, smooth_flags) + bitmask_smooth( + output, + objtree, + bump, + neighborhood, + atom, + adjacencies, + smooth_flags, + ) } else { true } @@ -80,9 +95,18 @@ impl RenderPass for IconSmoothing { // ---------------------------------------------------------------------------- // Older cardinal smoothing system -fn calculate_adjacencies(objtree: &ObjectTree, neighborhood: &Neighborhood, atom: &Atom, smooth_flags: i32) -> i32 { +fn calculate_adjacencies( + objtree: &ObjectTree, + neighborhood: &Neighborhood, + atom: &Atom, + smooth_flags: i32, +) -> i32 { + // Easier to read as a nested conditional + #[allow(clippy::collapsible_if)] if atom.istype("/atom/movable/") { - if atom.get_var("can_be_unanchored", objtree).to_bool() && !atom.get_var("anchored", objtree).to_bool() { + if atom.get_var("can_be_unanchored", objtree).to_bool() + && !atom.get_var("anchored", objtree).to_bool() + { return 0; } } @@ -121,19 +145,25 @@ fn calculate_adjacencies(objtree: &ObjectTree, neighborhood: &Neighborhood, atom adjacencies } -fn find_type_in_direction(objtree: &ObjectTree, adjacency: &Neighborhood, source: &Atom, direction: Dir, smooth_flags: i32) -> bool { +fn find_type_in_direction( + objtree: &ObjectTree, + adjacency: &Neighborhood, + source: &Atom, + direction: Dir, + smooth_flags: i32, +) -> bool { let atom_list = adjacency.offset(direction); if atom_list.is_empty() { return smooth_flags & SMOOTH_BORDER != 0; } match source.get_var("canSmoothWith", objtree) { - &Constant::List(ref elements) => { + Constant::List(elements) => { // smooth with anything for which their smoothing_groups overlaps our canSmoothWith - let set: std::collections::HashSet<_> = elements.iter().map(|x| &x.0).collect(); + let set: HashSet<_> = elements.iter().map(|x| &x.0).collect(); for atom in atom_list { - if let &Constant::List(ref elements2) = atom.get_var("smoothing_groups", objtree) { - let set2: std::collections::HashSet<_> = elements2.iter().map(|x| &x.0).collect(); + if let Constant::List(elements2) = atom.get_var("smoothing_groups", objtree) { + let set2: HashSet<_> = elements2.iter().map(|x| &x.0).collect(); if set.intersection(&set2).next().is_some() { return true; } @@ -152,12 +182,46 @@ fn find_type_in_direction(objtree: &ObjectTree, adjacency: &Neighborhood, source false } -fn cardinal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bump: &'a bumpalo::Bump, source: &Atom<'a>, adjacencies: i32) { +fn cardinal_smooth<'a>( + output: &mut Vec>, + objtree: &'a ObjectTree, + bump: &'a bumpalo::Bump, + source: &Atom<'a>, + adjacencies: i32, +) { for &(what, f1, n1, f2, n2, f3) in &[ - ("1", NORTH_JUNCTION, "n", WEST_JUNCTION, "w", NORTHWEST_JUNCTION), - ("2", NORTH_JUNCTION, "n", EAST_JUNCTION, "e", NORTHEAST_JUNCTION), - ("3", SOUTH_JUNCTION, "s", WEST_JUNCTION, "w", SOUTHWEST_JUNCTION), - ("4", SOUTH_JUNCTION, "s", EAST_JUNCTION, "e", SOUTHEAST_JUNCTION), + ( + "1", + NORTH_JUNCTION, + "n", + WEST_JUNCTION, + "w", + NORTHWEST_JUNCTION, + ), + ( + "2", + NORTH_JUNCTION, + "n", + EAST_JUNCTION, + "e", + NORTHEAST_JUNCTION, + ), + ( + "3", + SOUTH_JUNCTION, + "s", + WEST_JUNCTION, + "w", + SOUTHWEST_JUNCTION, + ), + ( + "4", + SOUTH_JUNCTION, + "s", + EAST_JUNCTION, + "e", + SOUTHEAST_JUNCTION, + ), ] { let name = if (adjacencies & f1 != 0) && (adjacencies & f2 != 0) { if (adjacencies & f3) != 0 { @@ -175,7 +239,7 @@ fn cardinal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu let mut sprite = Sprite { icon_state: name.into_bump_str(), - .. source.sprite + ..source.sprite }; if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() { sprite.icon = icon; @@ -184,7 +248,14 @@ fn cardinal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu } } -fn diagonal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bump: &'a bumpalo::Bump, neighborhood: &Neighborhood<'a, '_>, source: &Atom<'a>, adjacencies: i32) { +fn diagonal_smooth<'a>( + output: &mut Vec>, + objtree: &'a ObjectTree, + bump: &'a bumpalo::Bump, + neighborhood: &Neighborhood<'a, '_>, + source: &Atom<'a>, + adjacencies: i32, +) { let presets = if adjacencies == NORTH_JUNCTION | WEST_JUNCTION { ["d-se", "d-se-0"] } else if adjacencies == NORTH_JUNCTION | EAST_JUNCTION { @@ -214,7 +285,7 @@ fn diagonal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu for &each in presets.iter() { let mut copy = Sprite { icon_state: each, - .. source.sprite + ..source.sprite }; if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() { copy.icon = icon; @@ -223,14 +294,23 @@ fn diagonal_smooth<'a>(output: &mut Vec>, objtree: &'a ObjectTree, bu } } -fn diagonal_underlay<'a>(output: &mut Vec>, objtree: &'a ObjectTree, neighborhood: &Neighborhood<'a, '_>, source: &Atom<'a>, adjacencies: i32) { +fn diagonal_underlay<'a>( + output: &mut Vec>, + objtree: &'a ObjectTree, + neighborhood: &Neighborhood<'a, '_>, + source: &Atom<'a>, + adjacencies: i32, +) { // BYOND memes if source .get_var("fixed_underlay", objtree) .index(&Constant::string("space")) .is_some() { - output.push(Sprite::from_vars(objtree, &objtree.expect("/turf/open/space/basic"))); + output.push(Sprite::from_vars( + objtree, + &objtree.expect("/turf/open/space/basic"), + )); } else if let Some(dir) = reverse_ndir(adjacencies) { let dir = dir.flip(); let mut needs_plating = true; @@ -246,7 +326,10 @@ fn diagonal_underlay<'a>(output: &mut Vec>, objtree: &'a ObjectTree, } } if needs_plating { - output.push(Sprite::from_vars(objtree, &objtree.expect("/turf/open/floor/plating"))); + output.push(Sprite::from_vars( + objtree, + &objtree.expect("/turf/open/floor/plating"), + )); } } } @@ -288,18 +371,27 @@ fn bitmask_smooth<'a>( ) -> bool { let mut diagonal = ""; if source.istype("/turf/open/floor/") { - if source.get_var("broken", objtree).to_bool() || source.get_var("burnt", objtree).to_bool() { - return true; // use original appearance + if source.get_var("broken", objtree).to_bool() || source.get_var("burnt", objtree).to_bool() + { + return true; // use original appearance } - } else if source.istype("/turf/closed/") && (smooth_flags & SMOOTH_DIAGONAL_CORNERS != 0) && reverse_ndir(smoothing_junction).is_some() { + } else if source.istype("/turf/closed/") + && (smooth_flags & SMOOTH_DIAGONAL_CORNERS != 0) + && reverse_ndir(smoothing_junction).is_some() + { diagonal_underlay(output, objtree, neighborhood, source, smoothing_junction); diagonal = "-d"; } - let base_icon_state = source.get_var("base_icon_state", objtree).as_str().unwrap_or(""); + let base_icon_state = source + .get_var("base_icon_state", objtree) + .as_str() + .unwrap_or(""); let mut sprite = Sprite { - icon_state: bumpalo::format!(in bump, "{}-{}{}", base_icon_state, smoothing_junction, diagonal).into_bump_str(), - .. source.sprite + icon_state: + bumpalo::format!(in bump, "{}-{}{}", base_icon_state, smoothing_junction, diagonal) + .into_bump_str(), + ..source.sprite }; if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() { sprite.icon = icon; diff --git a/crates/dmm-tools/src/render_passes/mod.rs b/crates/dmm-tools/src/render_passes/mod.rs index 116746b3..a4119e8d 100644 --- a/crates/dmm-tools/src/render_passes/mod.rs +++ b/crates/dmm-tools/src/render_passes/mod.rs @@ -1,20 +1,20 @@ -use dm::objtree::*; +use crate::minimap::{Atom, GetVar, Layer, Neighborhood, Sprite}; use dm::constants::Constant; -use crate::minimap::{Atom, GetVar, Sprite, Layer, Neighborhood}; +use dm::objtree::*; -mod transit_tube; -mod random; -mod structures; mod icon_smoothing; mod icon_smoothing_2020; +mod random; mod smart_cables; +mod structures; +mod transit_tube; -pub use self::transit_tube::TransitTube; -pub use self::random::Random; -pub use self::structures::{GravityGen, Spawners}; pub use self::icon_smoothing::IconSmoothing as IconSmoothing2016; pub use self::icon_smoothing_2020::IconSmoothing; +pub use self::random::Random; pub use self::smart_cables::SmartCables; +pub use self::structures::{GravityGen, Spawners}; +pub use self::transit_tube::TransitTube; /// A map rendering pass. /// @@ -26,61 +26,69 @@ pub trait RenderPass: Sync { fn configure(&mut self, renderer_config: &dm::config::MapRenderer) {} /// Filter atoms based solely on their typepath. - fn path_filter(&self, - path: &str, - ) -> bool { true } + fn path_filter(&self, path: &str) -> bool { + true + } /// Filter atoms at the beginning of the process. /// /// Return `false` to discard the atom. - fn early_filter(&self, - atom: &Atom, - objtree: &ObjectTree, - ) -> bool { true } + fn early_filter(&self, atom: &Atom, objtree: &ObjectTree) -> bool { + true + } /// Expand atoms, such as spawners into the atoms they spawn. /// /// Return `false` to discard the original atom. - fn expand<'a>(&self, + fn expand<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, output: &mut Vec>, - ) -> bool { true } + ) -> bool { + true + } - fn adjust_sprite<'a>(&self, + fn adjust_sprite<'a>( + &self, atom: &Atom<'a>, sprite: &mut Sprite<'a>, objtree: &'a ObjectTree, - bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this - ) {} + bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this + ) { + } /// Apply overlays and underlays to an atom, in the form of pseudo-atoms. - fn overlays<'a>(&self, + fn overlays<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, underlays: &mut Vec>, overlays: &mut Vec>, - bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this - ) {} + bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this + ) { + } - fn neighborhood_appearance<'a>(&self, + fn neighborhood_appearance<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, neighborhood: &Neighborhood<'a, '_>, output: &mut Vec>, - bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this - ) -> bool { true } + bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this + ) -> bool { + true + } /// Filter atoms at the end of the process, after they have been taken into /// account by their neighbors. - fn late_filter(&self, - atom: &Atom, - objtree: &ObjectTree, - ) -> bool { true } + fn late_filter(&self, atom: &Atom, objtree: &ObjectTree) -> bool { + true + } - fn sprite_filter(&self, - sprite: &Sprite, - ) -> bool { true } + fn sprite_filter(&self, sprite: &Sprite) -> bool { + true + } } pub struct RenderPassInfo { @@ -91,40 +99,120 @@ pub struct RenderPassInfo { } macro_rules! pass { - ($typ:ty, $name:expr, $desc:expr, $def:expr) => (RenderPassInfo { - name: $name, - desc: $desc, - default: $def, - new: || Box::new(<$typ>::default()) - }) + ($typ:ty, $name:expr, $desc:expr, $def:expr) => { + RenderPassInfo { + name: $name, + desc: $desc, + default: $def, + new: || Box::<$typ>::default(), + } + }; } pub const RENDER_PASSES: &[RenderPassInfo] = &[ - pass!(HideSpace, "hide-space", "Do not render space tiles, instead leaving transparency.", true), + pass!( + HideSpace, + "hide-space", + "Do not render space tiles, instead leaving transparency.", + true + ), pass!(HideAreas, "hide-areas", "Do not render area icons.", true), - pass!(HideInvisible, "hide-invisible", "Do not render invisible or ephemeral objects such as mapping helpers.", true), - pass!(Random, "random", "Replace random spawners with one of their possibilities.", true), - pass!(Pretty, "pretty", "Add the minor cosmetic overlays for various objects.", true), - pass!(Spawners, "spawners", "Replace object spawners with their spawned objects.", true), - pass!(Overlays, "overlays", "Add overlays and underlays to atoms which usually have them.", true), - pass!(TransitTube, "transit-tube", "Add overlays to connect transit tubes together.", true), - pass!(GravityGen, "gravity-gen", "Expand the gravity generator to the full structure.", true), + pass!( + HideInvisible, + "hide-invisible", + "Do not render invisible or ephemeral objects such as mapping helpers.", + true + ), + pass!( + Random, + "random", + "Replace random spawners with one of their possibilities.", + true + ), + pass!( + Pretty, + "pretty", + "Add the minor cosmetic overlays for various objects.", + true + ), + pass!( + Spawners, + "spawners", + "Replace object spawners with their spawned objects.", + true + ), + pass!( + Overlays, + "overlays", + "Add overlays and underlays to atoms which usually have them.", + true + ), + pass!( + TransitTube, + "transit-tube", + "Add overlays to connect transit tubes together.", + true + ), + pass!( + GravityGen, + "gravity-gen", + "Expand the gravity generator to the full structure.", + true + ), pass!(Wires, "only-powernet", "Render only power cables.", false), - pass!(Pipes, "only-pipenet", "Render only atmospheric pipes.", false), - pass!(FancyLayers, "fancy-layers", "Layer atoms according to in-game rules.", true), - pass!(IconSmoothing2016, "icon-smoothing-2016", "Emulate the icon smoothing subsystem (xxalpha, 2016).", false), - pass!(IconSmoothing, "icon-smoothing", "Emulate the icon smoothing subsystem (Rohesie, 2020).", true), - pass!(SmartCables, "smart-cables", "Handle smart cable layout.", true), - pass!(WiresAndPipes, "only-wires-and-pipes", "Renders only power cables and atmospheric pipes.", false), + pass!( + Pipes, + "only-pipenet", + "Render only atmospheric pipes.", + false + ), + pass!( + FancyLayers, + "fancy-layers", + "Layer atoms according to in-game rules.", + true + ), + pass!( + IconSmoothing2016, + "icon-smoothing-2016", + "Emulate the icon smoothing subsystem (xxalpha, 2016).", + false + ), + pass!( + IconSmoothing, + "icon-smoothing", + "Emulate the icon smoothing subsystem (Rohesie, 2020).", + true + ), + pass!( + SmartCables, + "smart-cables", + "Handle smart cable layout.", + true + ), + pass!( + WiresAndPipes, + "only-wires-and-pipes", + "Renders only power cables and atmospheric pipes.", + false + ), ]; -pub fn configure(renderer_config: &dm::config::MapRenderer, include: &str, exclude: &str) -> Vec> { - let include: Vec<&str> = include.split(",").collect(); - let exclude: Vec<&str> = exclude.split(",").collect(); +pub fn configure( + renderer_config: &dm::config::MapRenderer, + include: &str, + exclude: &str, +) -> Vec> { + let include: Vec<&str> = include.split(',').collect(); + let exclude: Vec<&str> = exclude.split(',').collect(); configure_list(renderer_config, &include, &exclude) } -pub fn configure_list>(renderer_config: &dm::config::MapRenderer, include: &[T], exclude: &[T]) -> Vec> { +pub fn configure_list>( + renderer_config: &dm::config::MapRenderer, + include: &[T], + exclude: &[T], +) -> Vec> { let include_all = include.iter().any(|name| name.as_ref() == "all"); let exclude_all = exclude.iter().any(|name| name.as_ref() == "all"); @@ -155,14 +243,15 @@ pub fn configure_list>(renderer_config: &dm::config::MapRenderer, fn add_to<'a>(target: &mut Vec>, atom: &Atom<'a>, icon_state: &'a str) { target.push(Sprite { icon_state, - .. atom.sprite + ..atom.sprite }); } #[derive(Default)] pub struct HideSpace; impl RenderPass for HideSpace { - fn expand<'a>(&self, + fn expand<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, output: &mut Vec>, @@ -188,7 +277,7 @@ impl RenderPass for HideSpace { pub struct HideAreas; impl RenderPass for HideAreas { fn path_filter(&self, path: &str) -> bool { - !subpath(path, "/area/") + !ispath(path, "/area/") } } @@ -199,9 +288,10 @@ pub struct HideInvisible { impl RenderPass for HideInvisible { fn configure(&mut self, renderer_config: &dm::config::MapRenderer) { - self.overrides = renderer_config.hide_invisible.clone(); + self.overrides.clone_from(&renderer_config.hide_invisible); // Put longer typepaths earlier in the list so that `/foo/bar` can override `/foo`. - self.overrides.sort_unstable_by_key(|k| usize::MAX - k.len()); + self.overrides + .sort_unstable_by_key(|k| usize::MAX - k.len()); // Append `/` to each typepath for faster starts_with later. for key in self.overrides.iter_mut() { if !key.ends_with('/') { @@ -211,7 +301,7 @@ impl RenderPass for HideInvisible { } fn path_filter(&self, path: &str) -> bool { - !subpath(path, "/obj/effect/spawner/xmastree/") + !ispath(path, "/obj/effect/spawner/xmastree/") } fn early_filter(&self, atom: &Atom, objtree: &ObjectTree) -> bool { @@ -224,14 +314,18 @@ impl RenderPass for HideInvisible { } } // invisible objects and syndicate balloons are not to show - if atom.get_var("invisibility", objtree).to_float().unwrap_or(0.) > 60. || - atom.istype("/obj/effect/mapping_helpers/") + if atom + .get_var("invisibility", objtree) + .to_float() + .unwrap_or(0.) + > 60. + || atom.istype("/obj/effect/mapping_helpers/") { return false; } - if atom.get_var("icon", objtree) == "icons/obj/items_and_weapons.dmi" && - atom.get_var("icon_state", objtree) == "syndballoon" && - !atom.istype("/obj/item/toy/syndicateballoon/") + if atom.get_var("icon", objtree) == "icons/obj/items_and_weapons.dmi" + && atom.get_var("icon_state", objtree) == "syndballoon" + && !atom.istype("/obj/item/toy/syndicateballoon/") { return false; } @@ -242,7 +336,8 @@ impl RenderPass for HideInvisible { #[derive(Default)] pub struct Overlays; impl RenderPass for Overlays { - fn adjust_sprite<'a>(&self, + fn adjust_sprite<'a>( + &self, atom: &Atom<'a>, sprite: &mut Sprite<'a>, objtree: &'a ObjectTree, @@ -252,12 +347,16 @@ impl RenderPass for Overlays { if atom.istype("/obj/machinery/power/apc/") { // auto-set pixel location - match atom.get_var("dir", objtree).to_int().and_then(Dir::from_int) { + match atom + .get_var("dir", objtree) + .to_int() + .and_then(Dir::from_int) + { Some(Dir::North) => sprite.ofs_y = 23, Some(Dir::South) => sprite.ofs_y = -23, Some(Dir::East) => sprite.ofs_x = 24, Some(Dir::West) => sprite.ofs_x = -25, - _ => {} + _ => {}, } } } @@ -275,12 +374,12 @@ impl RenderPass for Overlays { underlays.push(Sprite { icon: "icons/turf/floors.dmi", icon_state: "plating", - .. atom.sprite + ..atom.sprite }); underlays.push(Sprite { icon: "icons/obj/structures.dmi", icon_state: "grille", - .. atom.sprite + ..atom.sprite }); } else if atom.istype("/obj/structure/closet/") { // closet doors @@ -290,20 +389,30 @@ impl RenderPass for Overlays { } else { "icon_state" }; - if let &Constant::String(ref door) = atom.get_var(var, objtree) { - add_to(overlays, atom, bumpalo::format!(in bump, "{}_open", door).into_bump_str()); + if let Constant::String(door) = atom.get_var(var, objtree) { + add_to( + overlays, + atom, + bumpalo::format!(in bump, "{}_open", door).into_bump_str(), + ); } } else { - if let &Constant::String(ref door) = atom + if let Constant::String(door) = atom .get_var_notnull("icon_door", objtree) .unwrap_or_else(|| atom.get_var("icon_state", objtree)) { - add_to(overlays, atom, bumpalo::format!(in bump, "{}_door", door).into_bump_str()); + add_to( + overlays, + atom, + bumpalo::format!(in bump, "{}_door", door).into_bump_str(), + ); } if atom.get_var("welded", objtree).to_bool() { add_to(overlays, atom, "welded"); } - if atom.get_var("secure", objtree).to_bool() && !atom.get_var("broken", objtree).to_bool() { + if atom.get_var("secure", objtree).to_bool() + && !atom.get_var("broken", objtree).to_bool() + { if atom.get_var("locked", objtree).to_bool() { add_to(overlays, atom, "locked"); } else { @@ -311,7 +420,9 @@ impl RenderPass for Overlays { } } } - } else if atom.istype("/obj/machinery/computer/") || atom.istype("/obj/machinery/power/solar_control/") { + } else if atom.istype("/obj/machinery/computer/") + || atom.istype("/obj/machinery/power/solar_control/") + { // computer screens and keyboards if let Some(screen) = atom.get_var("icon_screen", objtree).as_str() { add_to(overlays, atom, screen); @@ -325,7 +436,7 @@ impl RenderPass for Overlays { overlays.push(Sprite { icon: overlays_file, icon_state: "glass_closed", - .. atom.sprite + ..atom.sprite }) } } else { @@ -338,10 +449,12 @@ impl RenderPass for Overlays { } // APC terminals - let mut terminal = Sprite::from_vars(objtree, &objtree.expect("/obj/machinery/power/terminal")); + let mut terminal = + Sprite::from_vars(objtree, &objtree.expect("/obj/machinery/power/terminal")); terminal.dir = atom.sprite.dir; // TODO: un-hack this - FancyLayers::default().apply_fancy_layer("/obj/machinery/power/terminal", &mut terminal); + FancyLayers::default() + .apply_fancy_layer("/obj/machinery/power/terminal", &mut terminal); underlays.push(terminal); } } @@ -350,7 +463,8 @@ impl RenderPass for Overlays { #[derive(Default)] pub struct Pretty; impl RenderPass for Pretty { - fn adjust_sprite<'a>(&self, + fn adjust_sprite<'a>( + &self, atom: &Atom<'a>, sprite: &mut Sprite<'a>, _: &'a ObjectTree, @@ -361,18 +475,20 @@ impl RenderPass for Pretty { } } - fn overlays<'a>(&self, + fn overlays<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, _: &mut Vec>, overlays: &mut Vec>, _: &bumpalo::Bump, ) { - if atom.istype("/obj/item/storage/box/") && !atom.istype("/obj/item/storage/box/papersack/") { + if atom.istype("/obj/item/storage/box/") && !atom.istype("/obj/item/storage/box/papersack/") + { if let Some(icon_state) = atom.get_var("illustration", objtree).as_str() { overlays.push(Sprite { icon_state, - .. atom.sprite + ..atom.sprite }); } } else if atom.istype("/obj/machinery/firealarm/") { @@ -382,21 +498,21 @@ impl RenderPass for Pretty { } else if atom.istype("/obj/structure/tank_dispenser/") { if let &Constant::Float(oxygen) = atom.get_var("oxygentanks", objtree) { match oxygen as i32 { - 4..=std::i32::MAX => add_to(overlays, atom, "oxygen-4"), + 4..=i32::MAX => add_to(overlays, atom, "oxygen-4"), 3 => add_to(overlays, atom, "oxygen-3"), 2 => add_to(overlays, atom, "oxygen-2"), 1 => add_to(overlays, atom, "oxygen-1"), - _ => {} + _ => {}, } } if let &Constant::Float(plasma) = atom.get_var("plasmatanks", objtree) { match plasma as i32 { - 5..=std::i32::MAX => add_to(overlays, atom, "plasma-5"), + 5..=i32::MAX => add_to(overlays, atom, "plasma-5"), 4 => add_to(overlays, atom, "plasma-4"), 3 => add_to(overlays, atom, "plasma-3"), 2 => add_to(overlays, atom, "plasma-2"), 1 => add_to(overlays, atom, "plasma-1"), - _ => {} + _ => {}, } } } @@ -434,9 +550,14 @@ pub struct FancyLayers { impl RenderPass for FancyLayers { fn configure(&mut self, renderer_config: &dm::config::MapRenderer) { - self.overrides = renderer_config.fancy_layers.clone().into_iter().collect::>(); + self.overrides = renderer_config + .fancy_layers + .clone() + .into_iter() + .collect::>(); // Put longer typepaths earlier in the list so that `/foo/bar` can override `/foo`. - self.overrides.sort_unstable_by_key(|(k, _)| usize::MAX - k.len()); + self.overrides + .sort_unstable_by_key(|(k, _)| usize::MAX - k.len()); // Append `/` to each typepath for faster starts_with later. for (key, _) in self.overrides.iter_mut() { if !key.ends_with('/') { @@ -445,7 +566,8 @@ impl RenderPass for FancyLayers { } } - fn adjust_sprite<'a>(&self, + fn adjust_sprite<'a>( + &self, atom: &Atom<'a>, sprite: &mut Sprite<'a>, objtree: &'a ObjectTree, @@ -454,14 +576,15 @@ impl RenderPass for FancyLayers { self.apply_fancy_layer(atom.get_path(), sprite); // dual layering of vents 1: hide original sprite underfloor - if atom.istype("/obj/machinery/atmospherics/components/unary/") { - if unary_aboveground(atom, objtree).is_some() { - sprite.layer = Layer::from(-5); - } + if atom.istype("/obj/machinery/atmospherics/components/unary/") + && unary_aboveground(atom, objtree).is_some() + { + sprite.layer = Layer::from(-5); } } - fn overlays<'a>(&self, + fn overlays<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, _underlays: &mut Vec>, @@ -475,7 +598,7 @@ impl RenderPass for FancyLayers { icon_state: aboveground, // use original layer, not modified layer above layer: crate::minimap::layer_of(objtree, atom), - .. atom.sprite + ..atom.sprite }); } } @@ -484,10 +607,13 @@ impl RenderPass for FancyLayers { fn unary_aboveground(atom: &Atom, objtree: &ObjectTree) -> Option<&'static str> { Some(match atom.get_var("icon_state", objtree) { - &Constant::String(ref text) => match &**text { + Constant::String(text) => match &**text { "vent_map-1" | "vent_map-2" | "vent_map-3" | "vent_map-4" => "vent_off", "vent_map_on-1" | "vent_map_on-2" | "vent_map_on-3" | "vent_map_on-4" => "vent_out", - "vent_map_siphon_on-1" | "vent_map_siphon_on-2" | "vent_map_siphon_on-3" | "vent_map_siphon_on-4" => "vent_in", + "vent_map_siphon_on-1" + | "vent_map_siphon_on-2" + | "vent_map_siphon_on-3" + | "vent_map_siphon_on-4" => "vent_in", "scrub_map-1" | "scrub_map-2" | "scrub_map-3" | "scrub_map-4" => "scrub_off", "scrub_map_on-1" | "scrub_map_on-2" | "scrub_map_on-3" | "scrub_map_on-4" => "scrub_on", _ => return None, @@ -499,32 +625,32 @@ fn unary_aboveground(atom: &Atom, objtree: &ObjectTree) -> Option<&'static str> impl FancyLayers { fn fancy_layer_for_path(&self, p: &str) -> Option { for &(ref key, val) in self.overrides.iter() { - if subpath(p, key) { + if ispath(p, key) { return Some(Layer::from(val)); } } - if subpath(p, "/turf/open/floor/plating/") || subpath(p, "/turf/open/space/") { - Some(Layer::from(-10)) // under everything - } else if subpath(p, "/turf/closed/mineral/") { - Some(Layer::from(-3)) // above hidden stuff and plating but below walls - } else if subpath(p, "/turf/open/floor/") || subpath(p, "/turf/closed/") { - Some(Layer::from(-2)) // above hidden pipes and wires - } else if subpath(p, "/turf/") { - Some(Layer::from(-10)) // under everything - } else if subpath(p, "/obj/effect/turf_decal/") { - Some(Layer::from(-1)) // above turfs - } else if subpath(p, "/obj/structure/disposalpipe/") { + if ispath(p, "/turf/open/floor/plating/") || ispath(p, "/turf/open/space/") { + Some(Layer::from(-10)) // under everything + } else if ispath(p, "/turf/closed/mineral/") { + Some(Layer::from(-3)) // above hidden stuff and plating but below walls + } else if ispath(p, "/turf/open/floor/") || ispath(p, "/turf/closed/") { + Some(Layer::from(-2)) // above hidden pipes and wires + } else if ispath(p, "/turf/") { + Some(Layer::from(-10)) // under everything + } else if ispath(p, "/obj/effect/turf_decal/") { + Some(Layer::from(-1)) // above turfs + } else if ispath(p, "/obj/structure/disposalpipe/") { Some(Layer::from(-6)) - } else if subpath(p, "/obj/machinery/atmospherics/pipe/") && !p.contains("visible") { + } else if ispath(p, "/obj/machinery/atmospherics/pipe/") && !p.contains("visible") { Some(Layer::from(-5)) - } else if subpath(p, "/obj/structure/cable/") { + } else if ispath(p, "/obj/structure/cable/") { Some(Layer::from(-4)) - } else if subpath(p, "/obj/machinery/power/terminal/") { + } else if ispath(p, "/obj/machinery/power/terminal/") { Some(Layer::from(-3.5)) - } else if subpath(p, "/obj/structure/lattice/") { + } else if ispath(p, "/obj/structure/lattice/") { Some(Layer::from(-8)) - } else if subpath(p, "/obj/machinery/navbeacon/") { + } else if ispath(p, "/obj/machinery/navbeacon/") { Some(Layer::from(-3)) } else { None diff --git a/crates/dmm-tools/src/render_passes/random.rs b/crates/dmm-tools/src/render_passes/random.rs index bd34282f..30f81b7c 100644 --- a/crates/dmm-tools/src/render_passes/random.rs +++ b/crates/dmm-tools/src/render_passes/random.rs @@ -1,12 +1,13 @@ use super::*; -use rand::Rng; use rand::seq::SliceRandom; +use rand::Rng; #[derive(Default)] pub struct Random; impl RenderPass for Random { - fn expand<'a>(&self, + fn expand<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, output: &mut Vec>, @@ -23,7 +24,7 @@ impl RenderPass for Random { } if let Some(&replacement) = machines.choose(&mut rng) { output.push(Atom::from(replacement)); - return false; // consumed + return false; // consumed } } } else if atom.istype("/obj/machinery/vending/cola/random/") { @@ -36,12 +37,12 @@ impl RenderPass for Random { } if let Some(&replacement) = machines.choose(&mut rng) { output.push(Atom::from(replacement)); - return false; // consumed + return false; // consumed } } } else if atom.istype("/obj/item/bedsheet/random/") { if let Some(root) = objtree.find("/obj/item/bedsheet") { - let mut sheets = vec![root.get()]; // basic bedsheet is included + let mut sheets = vec![root.get()]; // basic bedsheet is included for child in root.children() { if child.name() != "random" { sheets.push(child.get()); @@ -49,7 +50,7 @@ impl RenderPass for Random { } if let Some(&replacement) = sheets.choose(&mut rng) { output.push(Atom::from(replacement)); - return false; // consumed + return false; // consumed } } } else if atom.istype("/obj/effect/spawner/lootdrop/") { @@ -83,12 +84,13 @@ impl RenderPass for Random { loot_spawned += 1; } } - return false; // consumed + return false; // consumed } true } - fn adjust_sprite<'a>(&self, + fn adjust_sprite<'a>( + &self, atom: &Atom<'a>, sprite: &mut Sprite<'a>, objtree: &'a ObjectTree, @@ -100,30 +102,39 @@ impl RenderPass for Random { const LEGIT_POSTERS: u32 = 35; if atom.istype("/obj/structure/sign/poster/contraband/random/") { - sprite.icon_state = bumpalo::format!(in bump, "poster{}", rng.gen_range(1..=CONTRABAND_POSTERS)).into_bump_str(); + sprite.icon_state = + bumpalo::format!(in bump, "poster{}", rng.gen_range(1..=CONTRABAND_POSTERS)) + .into_bump_str(); } else if atom.istype("/obj/structure/sign/poster/official/random/") { - sprite.icon_state = bumpalo::format!(in bump, "poster{}_legit", rng.gen_range(1..=LEGIT_POSTERS)).into_bump_str(); + sprite.icon_state = + bumpalo::format!(in bump, "poster{}_legit", rng.gen_range(1..=LEGIT_POSTERS)) + .into_bump_str(); } else if atom.istype("/obj/structure/sign/poster/random/") { let i = 1 + rng.gen_range(0..CONTRABAND_POSTERS + LEGIT_POSTERS); if i <= CONTRABAND_POSTERS { sprite.icon_state = bumpalo::format!(in bump, "poster{}", i).into_bump_str(); } else { - sprite.icon_state = bumpalo::format!(in bump, "poster{}_legit", i - CONTRABAND_POSTERS).into_bump_str(); + sprite.icon_state = + bumpalo::format!(in bump, "poster{}_legit", i - CONTRABAND_POSTERS) + .into_bump_str(); } - } else if atom.istype("/obj/item/kirbyplants/random/") || atom.istype("/obj/item/twohanded/required/kirbyplants/random/") { + } else if atom.istype("/obj/item/kirbyplants/random/") + || atom.istype("/obj/item/twohanded/required/kirbyplants/random/") + { sprite.icon = "icons/obj/flora/plants.dmi"; let random = rng.gen_range(0..26); if random == 0 { sprite.icon_state = "applebush"; } else { - sprite.icon_state = bumpalo::format!(in bump, "plant-{:02}", random).into_bump_str(); + sprite.icon_state = + bumpalo::format!(in bump, "plant-{:02}", random).into_bump_str(); } } else if atom.istype("/obj/structure/sign/barsign/") { if let Some(root) = objtree.find("/datum/barsign") { let mut signs = Vec::new(); for child in root.children() { if let Some(v) = child.vars.get("hidden") { - if !v.value.constant.as_ref().map_or(false, |c| c.to_bool()) { + if !v.value.constant.as_ref().is_some_and(|c| c.to_bool()) { continue; } } @@ -140,7 +151,7 @@ impl RenderPass for Random { } } } else if atom.istype("/obj/item/relic/") { - sprite.icon_state = [ + sprite.icon_state = [ "shock_kit", "armor-igniter-analyzer", "infra-igniter0", @@ -150,7 +161,9 @@ impl RenderPass for Random { "radio-radio", "timer-multitool0", "radio-igniter-tank", - ].choose(&mut rng).unwrap(); + ] + .choose(&mut rng) + .unwrap(); } if atom.istype("/obj/item/lipstick/random/") { @@ -163,16 +176,30 @@ impl RenderPass for Random { "tape_red", "tape_yellow", "tape_purple", - ].choose(&mut rng).unwrap(); + ] + .choose(&mut rng) + .unwrap(); } } } fn pickweight<'a>(list: &[&'a (Constant, Option)]) -> &'a Constant { - let mut total: i32 = list.iter().map(|(_, v)| v.as_ref().unwrap_or(Constant::null()).to_int().unwrap_or(1)).sum(); + let mut total: i32 = list + .iter() + .map(|(_, v)| { + v.as_ref() + .unwrap_or_else(Constant::null) + .to_int() + .unwrap_or(1) + }) + .sum(); total = rand::thread_rng().gen_range(1..=total); for (k, v) in list.iter() { - total -= v.as_ref().unwrap_or(Constant::null()).to_int().unwrap_or(1); + total -= v + .as_ref() + .unwrap_or_else(Constant::null) + .to_int() + .unwrap_or(1); if total <= 0 { return k; } diff --git a/crates/dmm-tools/src/render_passes/smart_cables.rs b/crates/dmm-tools/src/render_passes/smart_cables.rs index b1ee0e9e..bc6c717a 100644 --- a/crates/dmm-tools/src/render_passes/smart_cables.rs +++ b/crates/dmm-tools/src/render_passes/smart_cables.rs @@ -1,12 +1,13 @@ -use std::fmt::Write; -use crate::dmi::Dir; use super::*; +use crate::dmi::Dir; +use std::fmt::Write; #[derive(Default)] pub struct SmartCables; impl RenderPass for SmartCables { - fn neighborhood_appearance<'a>(&self, + fn neighborhood_appearance<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, neighborhood: &Neighborhood<'a, '_>, @@ -17,7 +18,10 @@ impl RenderPass for SmartCables { return true; } - let cable_layer = atom.get_var("cable_layer", objtree).as_str().unwrap_or("l2"); + let cable_layer = atom + .get_var("cable_layer", objtree) + .as_str() + .unwrap_or("l2"); let mut under_smes = false; let mut under_terminal = false; @@ -50,11 +54,15 @@ impl RenderPass for SmartCables { } for atom in turf { - if atom.istype("/obj/structure/cable/") { - if atom.get_var("cable_layer", objtree).as_str().unwrap_or("l2") == cable_layer { - linked_dirs |= check_dir.to_int(); - break; - } + if atom.istype("/obj/structure/cable/") + && atom + .get_var("cable_layer", objtree) + .as_str() + .unwrap_or("l2") + == cable_layer + { + linked_dirs |= check_dir.to_int(); + break; } } } @@ -79,7 +87,7 @@ impl RenderPass for SmartCables { output.push(Sprite { icon_state: icon_state.into_bump_str(), - .. atom.sprite + ..atom.sprite }); false } @@ -87,6 +95,8 @@ impl RenderPass for SmartCables { fn should_have_node(turf: &[Atom]) -> bool { for atom in turf { + // Readability, simple elif chain isn't duplicate code + #[allow(clippy::if_same_then_else)] if atom.istype("/obj/structure/grille/") || atom.istype("/obj/structure/cable_bridge/") { return true; } else if atom.istype("/obj/machinery/power/") { diff --git a/crates/dmm-tools/src/render_passes/structures.rs b/crates/dmm-tools/src/render_passes/structures.rs index b7cf08e3..92648a5e 100644 --- a/crates/dmm-tools/src/render_passes/structures.rs +++ b/crates/dmm-tools/src/render_passes/structures.rs @@ -3,7 +3,8 @@ use super::*; #[derive(Default)] pub struct Spawners; impl RenderPass for Spawners { - fn expand<'a>(&self, + fn expand<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, output: &mut Vec>, @@ -12,13 +13,13 @@ impl RenderPass for Spawners { return true; } match atom.get_var("spawn_list", objtree) { - &Constant::List(ref elements) => { - for &(ref key, _) in elements.iter() { + Constant::List(elements) => { + for (key, _) in elements.iter() { // TODO: use a more civilized lookup method let type_key; - let reference = match key { - &Constant::String(ref s) => s, - &Constant::Prefab(ref fab) => { + let reference = match *key { + Constant::String(ref s) => s, + Constant::Prefab(ref fab) => { type_key = dm::ast::FormatTreePath(&fab.path).to_string(); type_key.as_str() }, @@ -26,9 +27,9 @@ impl RenderPass for Spawners { }; output.push(Atom::from(objtree.expect(reference))); } - false // don't include the original atom - } - _ => { true } // TODO: complain? + false // don't include the original atom + }, + _ => true, // TODO: complain? } } } @@ -36,14 +37,15 @@ impl RenderPass for Spawners { #[derive(Default)] pub struct GravityGen; impl RenderPass for GravityGen { - fn overlays<'a>(&self, + fn overlays<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, _underlays: &mut Vec>, overlays: &mut Vec>, _: &bumpalo::Bump, ) { - if !atom.istype("/obj/machinery/gravity_generator/main/station/") { + if !atom.istype("/obj/machinery/gravity_generator/main/") { return; } @@ -57,13 +59,16 @@ impl RenderPass for GravityGen { (7, "on_7", 1, 0), (9, "on_9", -1, 0), ] { - let mut sprite = Sprite::from_vars(objtree, &objtree.expect("/obj/machinery/gravity_generator/part")); + let mut sprite = Sprite::from_vars( + objtree, + &objtree.expect("/obj/machinery/gravity_generator/part"), + ); sprite.ofs_x += 32 * x; sprite.ofs_y += 32 * y; sprite.icon_state = icon_state; - sprite.plane = 0; // TODO: figure out plane handling for real + sprite.plane = 0; // TODO: figure out plane handling for real if count <= 3 { - sprite.layer = Layer::from(4.25); // WALL_OBJ_LAYER + sprite.layer = Layer::from(4.25); // WALL_OBJ_LAYER } if count == 5 { // energy overlay goes above the middle part diff --git a/crates/dmm-tools/src/render_passes/transit_tube.rs b/crates/dmm-tools/src/render_passes/transit_tube.rs index b52a1950..b06f4a87 100644 --- a/crates/dmm-tools/src/render_passes/transit_tube.rs +++ b/crates/dmm-tools/src/render_passes/transit_tube.rs @@ -4,7 +4,8 @@ use crate::dmi::Dir; #[derive(Default)] pub struct TransitTube; impl RenderPass for TransitTube { - fn overlays<'a>(&self, + fn overlays<'a>( + &self, atom: &Atom<'a>, objtree: &'a ObjectTree, _: &mut Vec>, @@ -34,7 +35,11 @@ impl RenderPass for TransitTube { } }; - let dir = atom.get_var("dir", objtree).to_int().and_then(Dir::from_int).unwrap_or(Dir::default()); + let dir = atom + .get_var("dir", objtree) + .to_int() + .and_then(Dir::from_int) + .unwrap_or_default(); if atom.istype("/obj/structure/transit_tube/station/reverse/") { fulfill(&match dir { North => [East], @@ -109,7 +114,7 @@ fn create_tube_overlay<'a>( icon: source.sprite.icon, layer: source.sprite.layer, icon_state: "decorative", - .. Default::default() + ..Default::default() }; if let Some(shift) = shift { sprite.icon_state = "decorative_diag"; @@ -118,7 +123,7 @@ fn create_tube_overlay<'a>( Dir::South => sprite.ofs_y -= 32, Dir::East => sprite.ofs_x += 32, Dir::West => sprite.ofs_x -= 32, - _ => {} + _ => {}, } } output.push(sprite); diff --git a/crates/dmm-tools/tests/parse_codebase.rs b/crates/dmm-tools/tests/parse_codebase.rs index da30e0c7..3afb1bdc 100644 --- a/crates/dmm-tools/tests/parse_codebase.rs +++ b/crates/dmm-tools/tests/parse_codebase.rs @@ -1,9 +1,9 @@ -extern crate walkdir; extern crate dmm_tools; +extern crate walkdir; +use dmm_tools::*; use std::path::Path; use walkdir::{DirEntry, WalkDir}; -use dmm_tools::*; fn is_visible(entry: &DirEntry) -> bool { entry @@ -11,7 +11,7 @@ fn is_visible(entry: &DirEntry) -> bool { .file_name() .unwrap_or("".as_ref()) .to_str() - .map(|s| !s.starts_with(".")) + .map(|s| !s.starts_with('.')) .unwrap_or(true) } @@ -19,9 +19,9 @@ fn files_with_extension(ext: &str, mut f: F) { let dir = match std::env::var_os("TEST_DME") { Some(dme) => Path::new(&dme).parent().unwrap().to_owned(), None => { - println!("Set TEST_DME to check .{} files", ext); + println!("Set TEST_DME to check .{ext} files"); return; - } + }, }; for entry in WalkDir::new(dir).into_iter().filter_entry(is_visible) { let entry = entry.unwrap(); diff --git a/crates/dreamchecker/Cargo.toml b/crates/dreamchecker/Cargo.toml index 04bd65fc..3f9b9198 100644 --- a/crates/dreamchecker/Cargo.toml +++ b/crates/dreamchecker/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "dreamchecker" -version = "1.7.1" -authors = ["Tad Hardesty "] -edition = "2018" +version.workspace = true +authors.workspace = true +edition.workspace = true [dependencies] dreammaker = { path = "../dreammaker" } -guard = "0.5.0" serde_json = "1.0" -ahash = "0.7.6" +foldhash = "0.2.0" [build-dependencies] -chrono = "0.4.0" -git2 = { version = "0.13", default-features = false } +chrono = "0.4.38" +git2 = { version = "0.20.2", default-features = false } diff --git a/crates/dreamchecker/README.md b/crates/dreamchecker/README.md index 7af0d2e6..0e945142 100644 --- a/crates/dreamchecker/README.md +++ b/crates/dreamchecker/README.md @@ -3,7 +3,7 @@ **DreamChecker** is a robust whole-program static analysis and type checking engine for DreamMaker, the scripting language of the [BYOND] game engine. -[BYOND]: https://secure.byond.com/ +[BYOND]: https://www.byond.com/ ## Running DreamChecker diff --git a/crates/dreamchecker/build.rs b/crates/dreamchecker/build.rs index 71c828bd..df81ad5a 100644 --- a/crates/dreamchecker/build.rs +++ b/crates/dreamchecker/build.rs @@ -8,12 +8,12 @@ use std::path::PathBuf; fn main() { let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - let mut f = File::create(&out_dir.join("build-info.txt")).unwrap(); + let mut f = File::create(out_dir.join("build-info.txt")).unwrap(); if let Ok(commit) = read_commit() { - writeln!(f, "commit: {}", commit).unwrap(); + writeln!(f, "commit: {commit}").unwrap(); } - writeln!(f, "build date: {}", chrono::Utc::today()).unwrap(); + writeln!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap(); } fn read_commit() -> Result { diff --git a/crates/dreamchecker/src/lib.rs b/crates/dreamchecker/src/lib.rs index 86f52ffa..15380223 100644 --- a/crates/dreamchecker/src/lib.rs +++ b/crates/dreamchecker/src/lib.rs @@ -1,22 +1,22 @@ //! DreamChecker, a robust static analysis and typechecking engine for //! DreamMaker. #![allow(dead_code, unused_variables)] -#[macro_use] extern crate guard; extern crate dreammaker as dm; -use dm::{Context, DMError, Location, Severity}; -use dm::objtree::{ObjectTree, TypeRef, ProcRef}; -use dm::constants::{Constant, ConstFn}; use dm::ast::*; +use dm::constants::{ConstFn, Constant}; +use dm::objtree::{ObjectTree, ProcRef, TypeRef}; +use dm::{Context, DMError, Location, Severity}; -use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; - -use ahash::RandomState; +use foldhash::{HashMap, HashMapExt, HashSet, HashSetExt}; +use std::collections::{BTreeMap, VecDeque}; mod type_expr; use type_expr::TypeExpr; +mod switch_rand_range; +use switch_rand_range::check_switch_rand_range; -#[doc(hidden)] // Intended for the tests only. +#[doc(hidden)] // Intended for the tests only. pub mod test_helpers; // ---------------------------------------------------------------------------- @@ -31,14 +31,12 @@ pub enum StaticType<'o> { list: TypeRef<'o>, keys: Box>, }, + Proc, } impl<'o> StaticType<'o> { fn is_truthy(&self) -> bool { - match *self { - StaticType::None => false, - _ => true, - } + !matches!(*self, StaticType::None) } fn basic_type(&self) -> Option> { @@ -46,6 +44,7 @@ impl<'o> StaticType<'o> { StaticType::None => None, StaticType::Type(t) => Some(t), StaticType::List { list, .. } => Some(list), + StaticType::Proc => None, } } @@ -65,11 +64,17 @@ impl<'o> StaticType<'o> { } fn plain_list(tree: &'o ObjectTree) -> StaticType<'o> { - StaticType::List { list: tree.expect("/list"), keys: Box::new(StaticType::None) } + StaticType::List { + list: tree.expect("/list"), + keys: Box::new(StaticType::None), + } } fn list_of_type(tree: &'o ObjectTree, of: &str) -> StaticType<'o> { - StaticType::List { list: tree.expect("/list"), keys: Box::new(StaticType::Type(tree.expect(of))) } + StaticType::List { + list: tree.expect("/list"), + keys: Box::new(StaticType::Type(tree.expect(of))), + } } fn is_list(&self) -> bool { @@ -77,6 +82,7 @@ impl<'o> StaticType<'o> { StaticType::None => false, StaticType::Type(ty) => ty.path == "/list", StaticType::List { .. } => true, + StaticType::Proc => false, } } } @@ -97,10 +103,10 @@ impl<'o> Assumption<'o> { use Assumption::*; match (self, other) { // trivial conflicts - (Truthy(a), Truthy(b)) | - (IsNull(a), IsNull(b)) | - (IsText(a), IsText(b)) | - (IsNum(a), IsNum(b)) => a != b, + (Truthy(a), Truthy(b)) + | (IsNull(a), IsNull(b)) + | (IsText(a), IsText(b)) + | (IsNum(a), IsNum(b)) => a != b, // null is always false (Truthy(true), IsNull(true)) => true, // can only be one of null, text, num @@ -108,12 +114,12 @@ impl<'o> Assumption<'o> { (IsNum(true), IsNull(true)) => true, (IsText(true), IsNull(true)) => true, // types and paths are truthy - (IsType(true, _), Truthy(false)) | - (IsType(true, _), IsNull(true)) | - (IsPath(true, _), Truthy(false)) | - (IsPath(true, _), Truthy(true)) => true, + (IsType(true, _), Truthy(false)) + | (IsType(true, _), IsNull(true)) + | (IsPath(true, _), Truthy(false)) + | (IsPath(true, _), Truthy(true)) => true, // no conflict after all - _ => false + _ => false, } } } @@ -132,19 +138,32 @@ macro_rules! assumption_set { } impl<'o> AssumptionSet<'o> { - fn from_constant(objtree: &'o ObjectTree, constant: &Constant, type_hint: Option>) -> AssumptionSet<'o> { + fn from_constant( + objtree: &'o ObjectTree, + constant: &Constant, + type_hint: Option>, + ) -> AssumptionSet<'o> { match constant { - Constant::Null(_) => assumption_set![Assumption::IsNull(true), Assumption::Truthy(false)], - Constant::String(val) => assumption_set![Assumption::IsText(true), Assumption::Truthy(!val.is_empty())], + Constant::Null(_) => { + assumption_set![Assumption::IsNull(true), Assumption::Truthy(false)] + }, + Constant::String(val) => assumption_set![ + Assumption::IsText(true), + Assumption::Truthy(!val.is_empty()) + ], Constant::Resource(_) => assumption_set![Assumption::Truthy(true)], - Constant::Float(val) => assumption_set![Assumption::IsNum(true), Assumption::Truthy(*val != 0.0)], + Constant::Float(val) => { + assumption_set![Assumption::IsNum(true), Assumption::Truthy(*val != 0.0)] + }, Constant::List(_) => AssumptionSet::from_valid_instance(objtree.expect("/list")), Constant::Call(func, _) => match func { ConstFn::Icon => AssumptionSet::from_valid_instance(objtree.expect("/icon")), ConstFn::Matrix => AssumptionSet::from_valid_instance(objtree.expect("/matrix")), ConstFn::Newlist => AssumptionSet::from_valid_instance(objtree.expect("/list")), ConstFn::Sound => AssumptionSet::from_valid_instance(objtree.expect("/sound")), - ConstFn::Generator => AssumptionSet::from_valid_instance(objtree.expect("/generator")), + ConstFn::Generator => { + AssumptionSet::from_valid_instance(objtree.expect("/generator")) + }, ConstFn::Filter => AssumptionSet::default(), ConstFn::File => AssumptionSet::default(), }, @@ -172,16 +191,17 @@ impl<'o> AssumptionSet<'o> { } fn from_valid_instance(ty: TypeRef<'o>) -> AssumptionSet<'o> { - assumption_set![Assumption::Truthy(true), Assumption::IsNull(false), Assumption::IsType(true, ty)] + assumption_set![ + Assumption::Truthy(true), + Assumption::IsNull(false), + Assumption::IsType(true, ty) + ] } - fn conflicts_with(&self, new: &Assumption) -> Option<&Assumption> { - for each in self.set.iter() { - if each.oneway_conflict(new) || new.oneway_conflict(each) { - return Some(each); - } - } - None + fn conflicts_with(&self, new: &Assumption) -> Option<&Assumption<'_>> { + self.set + .iter() + .find(|&each| each.oneway_conflict(new) || new.oneway_conflict(each)) } } @@ -224,10 +244,14 @@ impl<'o> Analysis<'o> { fn from_static_type_impure(ty: TypeRef<'o>) -> Analysis<'o> { let mut analysis = Analysis::from(StaticType::Type(ty)); analysis.is_impure = Some(true); - return analysis + analysis } - fn from_value(objtree: &'o ObjectTree, value: Constant, type_hint: Option>) -> Analysis<'o> { + fn from_value( + objtree: &'o ObjectTree, + value: Constant, + type_hint: Option>, + ) -> Analysis<'o> { Analysis { static_ty: StaticType::None, aset: AssumptionSet::from_constant(objtree, &value, type_hint), @@ -252,7 +276,8 @@ trait WithFixHint { impl WithFixHint for DMError { fn with_fix_hint(mut self, analysis: &Analysis) -> Self { if let Some((loc, desc)) = analysis.fix_hint.clone() { - if !loc.is_builtins() { // Don't try to tell people to edit the builtins. + if !loc.is_builtins() { + // Don't try to tell people to edit the builtins. self.add_note(loc, desc); } } @@ -267,7 +292,7 @@ trait WithFilterArgs { impl WithFilterArgs for DMError { fn with_filter_args(mut self, loc: Location, filtertype: &str) -> Self { // luckily lummox has made the anchor url match the type= value for each filter - self.add_note(loc, format!("See: http://www.byond.com/docs/ref/#/{{notes}}/filters/{} for the permitted arguments", filtertype)); + self.add_note(loc, format!("See: http://www.byond.com/docs/ref/#/{{notes}}/filters/{filtertype} for the permitted arguments")); self } } @@ -295,7 +320,7 @@ impl<'o> From> for Analysis<'o> { Analysis { aset, - static_ty: static_ty, + static_ty, fix_hint: None, value: None, is_impure: None, @@ -326,7 +351,7 @@ fn run_inner(context: &Context, objtree: &ObjectTree, cli: bool) { cli_println!("============================================================"); cli_println!("Analyzing variables...\n"); - check_var_defs(&objtree, &context); + check_var_defs(objtree, context); let mut analyzer = AnalyzeObjectTree::new(context, objtree); @@ -398,7 +423,12 @@ struct ProcDirective<'o> { } impl<'o> ProcDirective<'o> { - pub fn new(directive_string: &'static str, can_be_disabled: bool, set_at_definition: bool, can_be_global: bool) -> ProcDirective<'o> { + pub fn new( + directive_string: &'static str, + can_be_disabled: bool, + set_at_definition: bool, + can_be_global: bool, + ) -> ProcDirective<'o> { ProcDirective { directive: Default::default(), directive_string, @@ -408,21 +438,41 @@ impl<'o> ProcDirective<'o> { } } - pub fn insert(&mut self, proc: ProcRef<'o>, enable: bool, location: Location) -> Result<(), DMError> { + pub fn insert( + &mut self, + proc: ProcRef<'o>, + enable: bool, + location: Location, + ) -> Result<(), DMError> { if proc.ty().is_root() && !self.can_be_global { - return Err(error(location, format!("{} sets {}, which cannot be set on global procs", proc, self.directive_string)) - .with_errortype("incompatible_directive")) + return Err(error( + location, + format!( + "{} sets {}, which cannot be set on global procs", + proc, self.directive_string + ), + ) + .with_errortype("incompatible_directive")); } if !enable && !self.can_be_disabled { - return Err(error(location, format!("{} sets {} false, but it cannot be disabled.", proc, self.directive_string)) - .with_errortype("disabled_directive") - .set_severity(Severity::Warning)) + return Err(error( + location, + format!( + "{} sets {} false, but it cannot be disabled.", + proc, self.directive_string + ), + ) + .with_errortype("disabled_directive") + .set_severity(Severity::Warning)); } if let Some((_, originallocation)) = self.directive.get(&proc) { - return Err(error(location, format!("{} sets {} twice", proc, self.directive_string)) - .with_note(*originallocation, "first definition here") - .with_errortype("sets_directive_twice") - .set_severity(Severity::Warning)) + return Err(error( + location, + format!("{} sets {} twice", proc, self.directive_string), + ) + .with_note(*originallocation, "first definition here") + .with_errortype("sets_directive_twice") + .set_severity(Severity::Warning)); } self.directive.insert(proc, (enable, location)); Ok(()) @@ -438,7 +488,7 @@ impl<'o> ProcDirective<'o> { let mut next = Some(proc); while let Some(current) = next { if let Some(&(truthy, location)) = self.get(current) { - return Some((current, truthy, location)) + return Some((current, truthy, location)); } next = current.parent_proc(); } @@ -446,7 +496,7 @@ impl<'o> ProcDirective<'o> { } fn try_copy_from_parent(&mut self, proc: ProcRef<'o>) { - if self.directive.get(&proc).is_none() { + if !self.directive.contains_key(&proc) { if let Some(parent) = proc.parent_proc() { if let Some((_, true, location)) = self.get_self_or_parent(parent) { let _ = self.insert(proc, true, location); @@ -464,8 +514,8 @@ pub fn directive_value_to_truthy(expr: &Expression, location: Location) -> Resul Some(Term::Int(1)) => Ok(true), Some(Term::Ident(i)) if i == "FALSE" => Ok(false), Some(Term::Ident(i)) if i == "TRUE" => Ok(true), - _ => Err(error(location, format!("invalid value for set {:?}", expr)) - .set_severity(Severity::Warning)), + _ => Err(error(location, format!("invalid value for set {expr:?}")) + .set_severity(Severity::Warning)), } } @@ -483,28 +533,28 @@ impl<'o> CallStack<'o> { trait DMErrorExt { fn with_callstack(self, stack: &CallStack) -> Self; - fn with_blocking_builtins(self, blockers: &Vec<(String, Location)>) -> Self; - fn with_impure_operations(self, impures: &Vec<(String, Location)>) -> Self; + fn with_blocking_builtins(self, blockers: &[(String, Location)]) -> Self; + fn with_impure_operations(self, impures: &[(String, Location)]) -> Self; } impl DMErrorExt for DMError { fn with_callstack(mut self, stack: &CallStack) -> DMError { for (procref, location, new_context) in stack.call_stack.iter() { - self.add_note(*location, format!("{}() called here", procref)); + self.add_note(*location, format!("{procref}() called here")); } self } - fn with_blocking_builtins(mut self, blockers: &Vec<(String, Location)>) -> DMError { + fn with_blocking_builtins(mut self, blockers: &[(String, Location)]) -> DMError { for (procname, location) in blockers.iter() { - self.add_note(*location, format!("{}() called here", procname)); + self.add_note(*location, format!("{procname}() called here")); } self } - fn with_impure_operations(mut self, impures: &Vec<(String, Location)>) -> DMError { + fn with_impure_operations(mut self, impures: &[(String, Location)]) -> DMError { for (impure, location) in impures.iter() { - self.add_note(*location, format!("{} happens here", impure)); + self.add_note(*location, format!("{impure} happens here")); } self } @@ -517,7 +567,10 @@ pub struct ViolatingProcs<'o> { impl<'o> ViolatingProcs<'o> { pub fn insert_violator(&mut self, proc: ProcRef<'o>, builtin: &str, location: Location) { - self.violators.entry(proc).or_default().push((builtin.to_string(), location)); + self.violators + .entry(proc) + .or_default() + .push((builtin.to_string(), location)); } pub fn get_violators(&self, proc: ProcRef<'o>) -> Option<&Vec<(String, Location)>> { @@ -561,6 +614,7 @@ pub struct AnalyzeObjectTree<'o> { sleeping_procs: ViolatingProcs<'o>, impure_procs: ViolatingProcs<'o>, + /// Procs with waitfor=0 or waitfor=FALSE waitfor_procs: HashSet>, sleeping_overrides: ViolatingOverrides<'o>, @@ -570,20 +624,38 @@ pub struct AnalyzeObjectTree<'o> { impl<'o> AnalyzeObjectTree<'o> { pub fn new(context: &'o Context, objtree: &'o ObjectTree) -> Self { let mut return_type = HashMap::default(); - return_type.insert(objtree.root().get_proc("get_step").unwrap(), StaticType::Type(objtree.expect("/turf")).into()); + return_type.insert( + objtree.root().get_proc("get_step").unwrap(), + StaticType::Type(objtree.expect("/turf")).into(), + ); AnalyzeObjectTree { context, objtree, return_type, - must_call_parent: ProcDirective::new("SpacemanDMM_should_call_parent", true, false, false), - must_not_override: ProcDirective::new("SpacemanDMM_should_not_override", false, false, false), + must_call_parent: ProcDirective::new( + "SpacemanDMM_should_call_parent", + true, + false, + false, + ), + must_not_override: ProcDirective::new( + "SpacemanDMM_should_not_override", + false, + false, + false, + ), private: ProcDirective::new("SpacemanDMM_private_proc", false, true, false), protected: ProcDirective::new("SpacemanDMM_protected_proc", false, true, false), must_not_sleep: ProcDirective::new("SpacemanDMM_should_not_sleep", false, true, true), sleep_exempt: ProcDirective::new("SpacemanDMM_allowed_to_sleep", false, true, true), must_be_pure: ProcDirective::new("SpacemanDMM_should_be_pure", false, true, true), - can_be_redefined: ProcDirective::new("SpacemanDMM_can_be_redefined", false, false, false), + can_be_redefined: ProcDirective::new( + "SpacemanDMM_can_be_redefined", + false, + false, + false, + ), used_kwargs: Default::default(), call_tree: Default::default(), sleeping_procs: Default::default(), @@ -603,7 +675,13 @@ impl<'o> AnalyzeObjectTree<'o> { } #[inline] - fn add_directive_or_error(&mut self, proc: ProcRef<'o>, directive: &str, expr: &Expression, location: Location) { + fn add_directive_or_error( + &mut self, + proc: ProcRef<'o>, + directive: &str, + expr: &Expression, + location: Location, + ) { let procdirective = match directive { "SpacemanDMM_should_not_override" => &mut self.must_not_override, "SpacemanDMM_should_call_parent" => &mut self.must_call_parent, @@ -614,21 +692,24 @@ impl<'o> AnalyzeObjectTree<'o> { "SpacemanDMM_should_be_pure" => &mut self.must_be_pure, "SpacemanDMM_can_be_redefined" => &mut self.can_be_redefined, other => { - error(location, format!("unknown linter setting {:?}", directive)) + error(location, format!("unknown linter setting {directive:?}")) .with_errortype("unknown_linter_setting") .set_severity(Severity::Warning) .register(self.context); - return - } + return; + }, }; if procdirective.set_at_definition { if let Some(procdef) = &mut proc.get_declaration() { if procdef.location != proc.get().location { - error(location, format!("Can't define procs {} outside their initial definition", directive)) - .set_severity(Severity::Warning) - .register(self.context); - return + error( + location, + format!("Can't define procs {directive} outside their initial definition"), + ) + .set_severity(Severity::Warning) + .register(self.context); + return; } } } @@ -639,14 +720,16 @@ impl<'o> AnalyzeObjectTree<'o> { self.context.register_error(error); } }, - Err(error) => self.context.register_error(error.with_errortype("invalid_lint_directive_value")), + Err(error) => self + .context + .register_error(error.with_errortype("invalid_lint_directive_value")), } } pub fn check_proc_call_tree(&mut self) { for (procref, &(_, location)) in self.must_not_sleep.directive.iter() { if let Some(sleepvec) = self.sleeping_procs.get_violators(*procref) { - error(procref.get().location, format!("{} sets SpacemanDMM_should_not_sleep but calls blocking built-in(s)", procref)) + error(procref.get().location, format!("{procref} sets SpacemanDMM_should_not_sleep but calls blocking built-in(s)")) .with_note(location, "SpacemanDMM_should_not_sleep set here") .with_errortype("must_not_sleep") .with_blocking_builtins(sleepvec) @@ -663,37 +746,42 @@ impl<'o> AnalyzeObjectTree<'o> { } while let Some((nextproc, callstack, new_context)) = to_visit.pop_front() { if !visited.insert(nextproc) { - continue + continue; } - if let Some(_) = self.waitfor_procs.get(&nextproc) { - continue + if self.waitfor_procs.contains(&nextproc) { + continue; } - if let Some(_) = self.sleep_exempt.get(nextproc) { - continue + if self.sleep_exempt.get(nextproc).is_some() { + continue; } if new_context { - continue + continue; } if let Some(sleepvec) = self.sleeping_procs.get_violators(nextproc) { - error(procref.get().location, format!("{} sets SpacemanDMM_should_not_sleep but calls blocking proc {}", procref, nextproc)) + error(procref.get().location, format!("{procref} sets SpacemanDMM_should_not_sleep but calls blocking proc {nextproc}")) .with_note(location, "SpacemanDMM_should_not_sleep set here") .with_errortype("must_not_sleep") .with_callstack(&callstack) .with_blocking_builtins(sleepvec) .register(self.context) - } else if let Some(overridesleep) = self.sleeping_overrides.get_override_violators(nextproc) { + } else if let Some(overridesleep) = + self.sleeping_overrides.get_override_violators(nextproc) + { for child_violator in overridesleep { - if procref.ty().is_subtype_of(&nextproc.ty()) && !child_violator.ty().is_subtype_of(&procref.ty()) { - continue + if procref.ty().is_subtype_of(&nextproc.ty()) + && !child_violator.ty().is_subtype_of(&procref.ty()) + { + continue; } - error(procref.get().location, format!("{} calls {} which has override child proc that sleeps {}", procref, nextproc, child_violator)) + error(procref.get().location, format!("{procref} calls {nextproc} which has override child proc that sleeps {child_violator}")) .with_note(location, "SpacemanDMM_should_not_sleep set here") .with_errortype("must_not_sleep") .with_callstack(&callstack) .with_blocking_builtins(self.sleeping_procs.get_violators(*child_violator).unwrap()) .register(self.context) } - } else if let Some(calledvec) = self.call_tree.get(&nextproc) { + } + if let Some(calledvec) = self.call_tree.get(&nextproc) { for (proccalled, location, new_context) in calledvec.iter() { let mut newstack = callstack.clone(); newstack.add_step(*proccalled, *location, *new_context); @@ -705,11 +793,14 @@ impl<'o> AnalyzeObjectTree<'o> { for (procref, (_, location)) in self.must_be_pure.directive.iter() { if let Some(impurevec) = self.impure_procs.get_violators(*procref) { - error(procref.get().location, format!("{} does impure operations", procref)) - .with_errortype("must_be_pure") - .with_note(*location, "SpacemanDMM_should_be_pure set here") - .with_impure_operations(impurevec) - .register(self.context) + error( + procref.get().location, + format!("{procref} does impure operations"), + ) + .with_errortype("must_be_pure") + .with_note(*location, "SpacemanDMM_should_be_pure set here") + .with_impure_operations(impurevec) + .register(self.context) } let mut visited = HashSet::>::new(); let mut to_visit = VecDeque::<(ProcRef<'o>, CallStack)>::new(); @@ -722,28 +813,33 @@ impl<'o> AnalyzeObjectTree<'o> { } while let Some((nextproc, callstack)) = to_visit.pop_front() { if !visited.insert(nextproc) { - continue + continue; } if let Some(impurevec) = self.impure_procs.get_violators(nextproc) { - error(procref.get().location, format!("{} sets SpacemanDMM_should_be_pure but calls a {} that does impure operations", procref, nextproc)) + error(procref.get().location, format!("{procref} sets SpacemanDMM_should_be_pure but calls a {nextproc} that does impure operations")) .with_note(*location, "SpacemanDMM_should_be_pure set here") .with_errortype("must_be_pure") .with_callstack(&callstack) .with_impure_operations(impurevec) .register(self.context) - } else if let Some(overrideimpure) = self.impure_overrides.get_override_violators(nextproc) { + } else if let Some(overrideimpure) = + self.impure_overrides.get_override_violators(nextproc) + { for child_violator in overrideimpure { - if procref.ty().is_subtype_of(&nextproc.ty()) && !child_violator.ty().is_subtype_of(&procref.ty()) { - continue + if procref.ty().is_subtype_of(&nextproc.ty()) + && !child_violator.ty().is_subtype_of(&procref.ty()) + { + continue; } - error(procref.get().location, format!("{} calls {} which has override child proc that does impure operations {}", procref, nextproc, child_violator)) + error(procref.get().location, format!("{procref} calls {nextproc} which has override child proc that does impure operations {child_violator}")) .with_note(*location, "SpacemanDMM_should_not_pure set here") .with_errortype("must_be_pure") .with_callstack(&callstack) .with_blocking_builtins(self.impure_procs.get_violators(*child_violator).unwrap()) .register(self.context) } - } else if let Some(calledvec) = self.call_tree.get(&nextproc) { + } + if let Some(calledvec) = self.call_tree.get(&nextproc) { for (proccalled, location, new_context) in calledvec.iter() { let mut newstack = callstack.clone(); newstack.add_step(*proccalled, *location, *new_context); @@ -754,37 +850,81 @@ impl<'o> AnalyzeObjectTree<'o> { } } - /// Gather and store set directives for the given proc using the provided code body + /// Gather and store set directives for the given proc using the provided code body and already existing flags pub fn gather_settings(&mut self, proc: ProcRef<'o>, code: &'o [Spanned]) { + let proc_location = proc.get().location; + + // Need to extract OUR declaration, and not our parent's. so we do the stupid + if let Some(proc_type) = proc.ty().get().procs.get(proc.name()) { + if let Some(declaration) = &proc_type.declaration { + let proc_flags = declaration.flags; + if proc_flags.is_final() { + // lemon todo: this should run, but it doesn't appear to trigger an error like I'd want. needs looking into imo + if let Err(error) = self.must_not_override.insert(proc, true, proc_location) { + self.context.register_error(error); + } + } + } + } + + if let Some(decl) = proc.get_declaration() { + match &decl.return_type { + ProcReturnType::InputType(input_type) => { + if let Some(path) = input_type.to_typepath() { + if let Some(ty) = self.objtree.find(path) { + self.return_type + .insert(proc, TypeExpr::from(StaticType::Type(ty))); + } + } + }, + ProcReturnType::TypePath(bits) => { + if let Ok(ty) = crate::static_type(self.objtree, proc_location, bits) { + self.return_type.insert(proc, TypeExpr::from(ty)); + } + }, + } + } + for statement in code.iter() { - if let Statement::Setting { ref name, ref value, .. } = statement.elem { + if let Statement::Setting { + ref name, + ref value, + .. + } = statement.elem + { if name == "SpacemanDMM_return_type" { if let Some(Term::Prefab(fab)) = value.as_term() { - let bits: Vec<_> = fab.path.iter().map(|(_, name)| name.to_owned()).collect(); + let bits: Vec<_> = + fab.path.iter().map(|(_, name)| name.to_owned()).collect(); let ty = self.static_type(statement.location, &bits); self.return_type.insert(proc, TypeExpr::from(ty)); } else { match TypeExpr::compile(proc, statement.location, value) { - Ok(expr) => { self.return_type.insert(proc, expr); }, + Ok(expr) => { + self.return_type.insert(proc, expr); + }, Err(error) => error .with_component(dm::Component::DreamChecker) .register(self.context), } } } else if name.starts_with("SpacemanDMM_") { - self.add_directive_or_error(proc, &name.as_str(), value, statement.location); + self.add_directive_or_error(proc, name.as_str(), value, statement.location); } else if !KNOWN_SETTING_NAMES.contains(&name.as_str()) { - error(statement.location, format!("unknown setting {:?}", name)) + error(statement.location, format!("unknown setting {name:?}")) .set_severity(Severity::Warning) .register(self.context); } else { match name.as_str() { "background" | "waitfor" | "hidden" | "instant" | "popup_menu" => { - if let Err(_) = directive_value_to_truthy(value, statement.location) { - error(statement.location, format!("set {} must be 0/1/TRUE/FALSE", name.as_str())) - .set_severity(Severity::Warning) - .with_errortype("invalid_set_value") - .register(self.context); + if directive_value_to_truthy(value, statement.location).is_err() { + error( + statement.location, + format!("set {} must be 0/1/TRUE/FALSE", name.as_str()), + ) + .set_severity(Severity::Warning) + .with_errortype("invalid_set_value") + .register(self.context); } }, "name" | "category" | "desc" => { @@ -795,10 +935,16 @@ impl<'o> AnalyzeObjectTree<'o> { // category can be set null to hide it Term::Null if name.as_str() == "category" => {}, other => { - error(statement.location, format!("set {} must have a string value", name.as_str())) - .set_severity(Severity::Warning) - .with_errortype("invalid_set_value") - .register(self.context); + error( + statement.location, + format!( + "set {} must have a string value", + name.as_str() + ), + ) + .set_severity(Severity::Warning) + .with_errortype("invalid_set_value") + .register(self.context); }, } } @@ -825,7 +971,8 @@ impl<'o> AnalyzeObjectTree<'o> { /// Propagate violations make up the inheritence graph pub fn propagate_violations(&mut self, proc: ProcRef<'o>) { - if proc.name() == "New" { // New() propogates via ..() and causes weirdness + if proc.name() == "New" { + // New() propogates via ..() and causes weirdness return; } if self.sleeping_procs.get_violators(proc).is_some() { @@ -864,7 +1011,11 @@ impl<'o> AnalyzeObjectTree<'o> { if !missing.is_empty() { kwargs.bad_overrides_at.insert( proc.ty().path.to_owned(), - BadOverride { missing, location: proc.location }); + BadOverride { + missing, + location: proc.location, + }, + ); } } next = current.parent_proc(); @@ -875,21 +1026,26 @@ impl<'o> AnalyzeObjectTree<'o> { pub fn finish_check_kwargs(&self) { for (base_procname, kwarg_info) in self.used_kwargs.iter() { if kwarg_info.bad_overrides_at.is_empty() { - continue + continue; } // List out the child procs that are missing overrides. let msg = match kwarg_info.bad_overrides_at.len() { - 1 => format!("an override of {} is missing keyword args", base_procname), - len => format!("{} overrides of {} are missing keyword args", len, base_procname), + 1 => format!("an override of {base_procname} is missing keyword args"), + len => format!("{len} overrides of {base_procname} are missing keyword args"), }; - let mut error = error(kwarg_info.location, msg) - .with_errortype("override_missing_keyword_arg"); + let mut error = + error(kwarg_info.location, msg).with_errortype("override_missing_keyword_arg"); let mut missing = HashSet::new(); for (child_procname, bad_override) in kwarg_info.bad_overrides_at.iter() { - error.add_note(bad_override.location, format!("{} is missing \"{}\"", - child_procname, - bad_override.missing.join("\", \""))); + error.add_note( + bad_override.location, + format!( + "{} is missing \"{}\"", + child_procname, + bad_override.missing.join("\", \"") + ), + ); missing.extend(bad_override.missing.iter()); } @@ -897,14 +1053,19 @@ impl<'o> AnalyzeObjectTree<'o> { // there's not gonna be a problem. for (arg_name, called_at) in kwarg_info.called_at.iter() { if !missing.contains(arg_name) { - continue + continue; } if called_at.others > 0 { - error.add_note(called_at.location, format!("called with {:?} here, and {} other places", - arg_name, called_at.others)); + error.add_note( + called_at.location, + format!( + "called with {:?} here, and {} other places", + arg_name, called_at.others + ), + ); } else { - error.add_note(called_at.location, format!("called with {:?} here", arg_name)); + error.add_note(called_at.location, format!("called with {arg_name:?} here")); } } @@ -918,13 +1079,29 @@ impl<'o> AnalyzeObjectTree<'o> { Err(e) => { e.register(self.context); StaticType::None - } + }, } } } -fn static_type<'o>(objtree: &'o ObjectTree, location: Location, mut of: &[String]) -> Result, DMError> { - while !of.is_empty() && ["static", "global", "const", "tmp", "SpacemanDMM_final", "SpacemanDMM_private", "SpacemanDMM_protected"].contains(&&*of[0]) { +fn static_type<'o>( + objtree: &'o ObjectTree, + location: Location, + mut of: &[String], +) -> Result, DMError> { + while !of.is_empty() + && [ + "static", + "global", + "const", + "tmp", + "final", + "SpacemanDMM_final", + "SpacemanDMM_private", + "SpacemanDMM_protected", + ] + .contains(&&*of[0]) + { of = &of[1..]; } @@ -939,7 +1116,10 @@ fn static_type<'o>(objtree: &'o ObjectTree, location: Location, mut of: &[String } else if let Some(ty) = objtree.type_by_path(of) { Ok(StaticType::Type(ty)) } else { - Err(error(location, format!("undefined type: {}", FormatTreePath(of)))) + Err(error( + location, + format!("undefined type: {}", FormatTreePath(of)), + )) } } @@ -972,33 +1152,50 @@ pub fn check_var_defs(objtree: &ObjectTree, context: &Context) { continue; } - guard!(let Some(parentvar) = parent.vars.get(varname) - else { continue }); + let Some(parentvar) = parent.vars.get(varname) else { + continue; + }; - guard!(let Some(decl) = &parentvar.declaration - else { continue }); + let Some(decl) = &parentvar.declaration else { + continue; + }; if let Some(mydecl) = &typevar.declaration { if typevar.value.location.is_builtins() { continue; } - DMError::new(mydecl.location, format!("{} redeclares var {:?}", path, varname)) - .with_note(decl.location, format!("declared on {} here", parent.path)) - .register(context); + DMError::new( + mydecl.location, + format!("{path} redeclares var {varname:?}"), + ) + .with_note(decl.location, format!("declared on {} here", parent.path)) + .register(context); } if decl.var_type.flags.is_final() { - DMError::new(typevar.value.location, format!("{} overrides final var {:?}", path, varname)) - .with_errortype("final_var") - .with_note(decl.location, format!("declared final on {} here", parent.path)) - .register(context); + DMError::new( + typevar.value.location, + format!("{path} overrides final var {varname:?}"), + ) + .with_errortype("final_var") + .with_note( + decl.location, + format!("declared final on {} here", parent.path), + ) + .register(context); } if decl.var_type.flags.is_private() { - DMError::new(typevar.value.location, format!("{} overrides private var {:?}", path, varname)) - .with_errortype("private_var") - .with_note(decl.location, format!("declared private on {} here", parent.path)) - .register(context); + DMError::new( + typevar.value.location, + format!("{path} overrides private var {varname:?}"), + ) + .with_errortype("private_var") + .with_note( + decl.location, + format!("declared private on {} here", parent.path), + ) + .register(context); } } } @@ -1024,6 +1221,7 @@ impl ControlFlow { fuzzy: false, } } + pub fn allfalse() -> ControlFlow { ControlFlow { returns: false, @@ -1032,12 +1230,13 @@ impl ControlFlow { fuzzy: false, } } + pub fn terminates(&self) -> bool { - return !self.fuzzy && ( self.returns || self.continues || self.breaks ) + !self.fuzzy && (self.returns || self.continues || self.breaks) } pub fn terminates_loop(&self) -> bool { - return !self.fuzzy && ( self.returns || self.breaks ) + !self.fuzzy && (self.returns || self.breaks) } pub fn no_else(&mut self) { @@ -1101,7 +1300,10 @@ struct LocalVar<'o> { impl<'o> From> for LocalVar<'o> { fn from(analysis: Analysis<'o>) -> Self { - LocalVar { location: Location::default(), analysis } + LocalVar { + location: Location::default(), + analysis, + } } } @@ -1116,7 +1318,12 @@ struct AnalyzeProc<'o, 's> { } impl<'o, 's> AnalyzeProc<'o, 's> { - fn new(env: &'s mut AnalyzeObjectTree<'o>, context: &'o Context, objtree: &'o ObjectTree, proc_ref: ProcRef<'o>) -> Self { + fn new( + env: &'s mut AnalyzeObjectTree<'o>, + context: &'o Context, + objtree: &'o ObjectTree, + proc_ref: ProcRef<'o>, + ) -> Self { let ty = proc_ref.ty(); AnalyzeProc { @@ -1131,139 +1338,224 @@ impl<'o, 's> AnalyzeProc<'o, 's> { } pub fn run(&mut self, block: &'o [Spanned]) { - let mut local_vars = HashMap::::with_hasher(RandomState::default()); + let mut local_vars = HashMap::::new(); local_vars.insert(".".to_owned(), Analysis::empty().into()); - local_vars.insert("args".to_owned(), Analysis::from_static_type_impure(self.objtree.expect("/list")).into()); - local_vars.insert("usr".to_owned(), Analysis::from_static_type(self.objtree.expect("/mob")).into()); + local_vars.insert( + "args".to_owned(), + Analysis::from_static_type_impure(self.objtree.expect("/list")).into(), + ); + local_vars.insert( + "usr".to_owned(), + Analysis::from_static_type(self.objtree.expect("/mob")).into(), + ); + local_vars.insert( + "callee".to_owned(), + Analysis::from_static_type(self.objtree.expect("/callee")).into(), + ); + local_vars.insert( + "caller".to_owned(), + Analysis::from_static_type(self.objtree.expect("/callee")).into(), + ); if !self.ty.is_root() { local_vars.insert("src".to_owned(), Analysis::from_static_type(self.ty).into()); } - local_vars.insert("global".to_owned(), Analysis { - static_ty: StaticType::Type(self.objtree.root()), - aset: assumption_set![Assumption::IsNull(false)], - value: None, - fix_hint: None, - is_impure: Some(true), - }.into()); + local_vars.insert( + "global".to_owned(), + Analysis { + static_ty: StaticType::Type(self.objtree.root()), + aset: assumption_set![Assumption::IsNull(false)], + value: None, + fix_hint: None, + is_impure: Some(true), + } + .into(), + ); for param in self.proc_ref.get().parameters.iter() { let mut analysis = self.static_type(param.location, ¶m.var_type.type_path); analysis.is_impure = Some(true); // all params are impure - local_vars.insert(param.name.to_owned(), LocalVar { - location: self.proc_ref.location, - analysis, - }); + local_vars.insert( + param.name.to_owned(), + LocalVar { + location: self.proc_ref.location, + analysis, + }, + ); //println!("adding parameters {:#?}", self.local_vars); } - self.visit_block(block, &mut local_vars); + self.visit_block(block, &mut local_vars, true); //println!("purity {}", self.is_pure); if let Some(parent) = self.proc_ref.parent_proc() { - if let Some((proc, true, location)) = self.env.private.get_self_or_parent(self.proc_ref) { + if let Some((proc, true, location)) = self.env.private.get_self_or_parent(self.proc_ref) + { if proc != self.proc_ref { - error(self.proc_ref.location, format!("proc overrides private parent, prohibited by {}", proc)) + error( + self.proc_ref.location, + format!("proc overrides private parent, prohibited by {proc}"), + ) .with_note(location, "prohibited by this private_proc annotation") .with_errortype("private_proc") .register(self.context); } } - if let Some((proc, true, location)) = self.env.must_not_override.get_self_or_parent(self.proc_ref) { + if let Some((proc, true, location)) = + self.env.must_not_override.get_self_or_parent(self.proc_ref) + { if proc != self.proc_ref { - error(self.proc_ref.location, format!("proc overrides parent, prohibited by {}", proc)) - .with_note(location, "prohibited by this must_not_override annotation") - .with_errortype("must_not_override") - .register(self.context); + error( + self.proc_ref.location, + format!("proc overrides parent, prohibited by {proc}"), + ) + .with_note(location, "prohibited by this must_not_override annotation") + .with_errortype("must_not_override") + .register(self.context); } } if !self.calls_parent { - if let Some((proc, true, location)) = self.env.must_call_parent.get_self_or_parent(self.proc_ref) { - error(self.proc_ref.location, format!("proc never calls parent, required by {}", proc)) - .with_note(location, "required by this must_call_parent annotation") - .with_errortype("must_call_parent") - .register(self.context); + if let Some((proc, true, location)) = + self.env.must_call_parent.get_self_or_parent(self.proc_ref) + { + error( + self.proc_ref.location, + format!("proc never calls parent, required by {proc}"), + ) + .with_note(location, "required by this must_call_parent annotation") + .with_errortype("must_call_parent") + .register(self.context); } } - if !parent.is_builtin() && self.proc_ref.ty() == parent.ty() - && self.env.can_be_redefined.get_self_or_parent(self.proc_ref).is_none() { - error(self.proc_ref.location, format!("redefining proc {}/{}", self.ty, self.proc_ref.name())) - .with_errortype("redefined_proc") - .with_note(parent.location, "previous definition is here") - .set_severity(Severity::Hint) - .register(self.context); + if !parent.is_builtin() + && self.proc_ref.ty() == parent.ty() + && self + .env + .can_be_redefined + .get_self_or_parent(self.proc_ref) + .is_none() + { + error( + self.proc_ref.location, + format!("redefining proc {}/{}", self.ty, self.proc_ref.name()), + ) + .with_errortype("redefined_proc") + .with_note(parent.location, "previous definition is here") + .set_severity(Severity::Hint) + .register(self.context); } } } - fn visit_block(&mut self, block: &'o [Spanned], local_vars: &mut HashMap, RandomState>) -> ControlFlow { + fn visit_block( + &mut self, + block: &'o [Spanned], + local_vars: &mut HashMap>, + mut setting_allowed: bool, + ) -> ControlFlow { let mut term = ControlFlow::allfalse(); for stmt in block.iter() { if term.terminates() { - error(stmt.location,"possible unreachable code here") + error(stmt.location, "possible unreachable code here") .with_errortype("unreachable_code") .register(self.context); - return term // stop evaluating + return term; // stop evaluating + } + match &stmt.elem { + Statement::Setting { name, mode, value } => { + // Defines like SHOULD_CALL_PARENT currently only work at the top + // Built in settings like background can situationally work in control blocks but are already warned by dreammaker + if !setting_allowed && name.starts_with("SpacemanDMM_") { + error(stmt.location, "set statement not at the top of the proc") + .with_errortype("set_has_no_effect") + .register(self.context); + } + }, + _ => { + setting_allowed = false; + }, } let state = self.visit_statement(stmt.location, &stmt.elem, local_vars); term.merge(state); } - return term + term } fn loop_condition_check(&mut self, location: Location, expression: &'o Expression) { match expression.is_truthy() { Some(true) => { - error(location,"loop condition is always true") + error(location, "loop condition is always true") .with_errortype("loop_condition_determinate") .register(self.context); }, Some(false) => { - error(location,"loop condition is always false") + error(location, "loop condition is always false") .with_errortype("loop_condition_determinate") .register(self.context); - } - _ => () + }, + _ => (), }; } fn visit_control_condition(&mut self, location: Location, expression: &'o Expression) { if expression.is_const_eval() { - error(location,"control flow condition is a constant evalutation") + error(location, "control flow condition is a constant evalutation") .with_errortype("control_condition_static") .register(self.context); - } - else if let Some(term) = expression.as_term() { + } else if let Some(term) = expression.as_term() { if term.is_static() { - error(location,"control flow condition is a static term") + error(location, "control flow condition is a static term") .with_errortype("control_condition_static") .register(self.context); } } } - fn visit_statement(&mut self, location: Location, statement: &'o Statement, local_vars: &mut HashMap, RandomState>) -> ControlFlow { + fn visit_statement( + &mut self, + location: Location, + statement: &'o Statement, + local_vars: &mut HashMap>, + ) -> ControlFlow { match statement { Statement::Expr(expr) => { match expr { Expression::Base { term, follow } => { if let Term::Call(call, vec) = &term.elem { - if !follow.iter().any(|f| match f.elem { Follow::Call(..) => true, _ => false }) { + if !follow.iter().any(|f| matches!(f.elem, Follow::Call(..))) { if let Some(proc) = self.ty.get_proc(call) { - if let Some((_, _, loc)) = self.env.must_be_pure.get_self_or_parent(proc) { - error(location, format!("call to pure proc {} discards return value", call)) - .with_note(loc, "prohibited by this must_be_pure annotation") - .register(self.context); + if let Some((_, _, loc)) = + self.env.must_be_pure.get_self_or_parent(proc) + { + error( + location, + format!( + "call to pure proc {call} discards return value" + ), + ) + .with_note( + loc, + "prohibited by this must_be_pure annotation", + ) + .register(self.context); } } } } }, - Expression::BinaryOp { op: BinaryOp::LShift, lhs, rhs } => { + Expression::BinaryOp { + op: BinaryOp::LShift, + lhs, + rhs, + } => { let lhsanalysis = self.visit_expression(location, lhs, None, local_vars); if let Some(impurity) = lhsanalysis.is_impure { if impurity { - self.env.impure_procs.insert_violator(self.proc_ref, "purity breaking << on expression", location); + self.env.impure_procs.insert_violator( + self.proc_ref, + "purity breaking << on expression", + location, + ); } } }, @@ -1280,36 +1572,63 @@ impl<'o, 's> AnalyzeProc<'o, 's> { } let return_type = self.visit_expression(location, expr, None, local_vars); local_vars.get_mut(".").unwrap().analysis = return_type; - return ControlFlow { returns: true, continues: false, breaks: false, fuzzy: false } + return ControlFlow { + returns: true, + continues: false, + breaks: false, + fuzzy: false, + }; + }, + Statement::Return(None) => { + return ControlFlow { + returns: true, + continues: false, + breaks: false, + fuzzy: false, + } }, - Statement::Return(None) => { return ControlFlow { returns: true, continues: false, breaks: false, fuzzy: false } }, Statement::Crash(expr) => { if let Some(expr) = expr { self.visit_expression(location, expr, None, local_vars); } - return ControlFlow { returns: true, continues: false, breaks: false, fuzzy: false } + return ControlFlow { + returns: true, + continues: false, + breaks: false, + fuzzy: false, + }; + }, + Statement::Throw(expr) => { + self.visit_expression(location, expr, None, local_vars); }, - Statement::Throw(expr) => { self.visit_expression(location, expr, None, local_vars); }, Statement::While { condition, block } => { let mut scoped_locals = local_vars.clone(); // We don't check for static/determine conditions because while(TRUE) is so common. self.visit_expression(location, condition, None, &mut scoped_locals); - let mut state = self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals, false); state.end_loop(); - return state + return state; }, Statement::DoWhile { block, condition } => { let mut scoped_locals = local_vars.clone(); - let mut state = self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals, false); if state.terminates_loop() { - error(location,"do while terminates without ever reaching condition") - .register(self.context); - return state + error( + location, + "do while terminates without ever reaching condition", + ) + .register(self.context); + return state; } - self.visit_expression(condition.location, &condition.elem, None, &mut scoped_locals); + self.visit_expression( + condition.location, + &condition.elem, + None, + &mut scoped_locals, + ); state.end_loop(); - return state + return state; }, Statement::If { arms, else_arm } => { let mut allterm = ControlFlow::alltrue(); @@ -1322,22 +1641,27 @@ impl<'o, 's> AnalyzeProc<'o, 's> { .with_errortype("unreachable_code") .register(self.context); } - self.visit_expression(condition.location, &condition.elem, None, &mut scoped_locals); - let state = self.visit_block(block, &mut scoped_locals); + self.visit_expression( + condition.location, + &condition.elem, + None, + &mut scoped_locals, + ); + let state = self.visit_block(block, &mut scoped_locals, false); match condition.elem.is_truthy() { Some(true) => { - error(condition.location,"if condition is always true") + error(condition.location, "if condition is always true") .with_errortype("if_condition_determinate") .register(self.context); allterm.merge_false(state); alwaystrue = true; }, Some(false) => { - error(condition.location,"if condition is always false") + error(condition.location, "if condition is always false") .with_errortype("if_condition_determinate") .register(self.context); }, - None => allterm.merge_false(state) + None => allterm.merge_false(state), }; } if let Some(else_arm) = else_arm { @@ -1348,22 +1672,27 @@ impl<'o, 's> AnalyzeProc<'o, 's> { .register(self.context); } } - let state = self.visit_block(else_arm, &mut local_vars.clone()); + let state = self.visit_block(else_arm, &mut local_vars.clone(), false); allterm.merge_false(state); } else { allterm.no_else(); - return allterm + return allterm; } allterm.finalize(); - return allterm + return allterm; }, Statement::ForInfinite { block } => { let mut scoped_locals = local_vars.clone(); - let mut state = self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals, false); state.end_loop(); - return state - } - Statement::ForLoop { init, test, inc, block } => { + return state; + }, + Statement::ForLoop { + init, + test, + inc, + block, + } => { let mut scoped_locals = local_vars.clone(); if let Some(init) = init { self.visit_statement(location, init, &mut scoped_locals); @@ -1376,12 +1705,18 @@ impl<'o, 's> AnalyzeProc<'o, 's> { if let Some(inc) = inc { self.visit_statement(location, inc, &mut scoped_locals); } - let mut state = self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals, false); state.end_loop(); - return state + return state; }, Statement::ForList(for_list) => { - let ForListStatement { var_type, name, input_type, in_list, block } = &**for_list; + let ForListStatement { + var_type, + name, + input_type, + in_list, + block, + } = &**for_list; let mut scoped_locals = local_vars.clone(); if let Some(in_list) = in_list { let list = self.visit_expression(location, in_list, None, &mut scoped_locals); @@ -1389,10 +1724,13 @@ impl<'o, 's> AnalyzeProc<'o, 's> { StaticType::None => { // Occurs extremely often due to DM not complaining about this, with // over 800 detections on /tg/. Maybe a future lint. - } - StaticType::List { .. } => {/* OK */} + }, + StaticType::List { .. } => { /* OK */ }, StaticType::Type(ty) => { - if ty != self.objtree.expect("/world") && ty != self.objtree.expect("/list") { + if ty != self.objtree.expect("/world") + && ty != self.objtree.expect("/list") + && ty != self.objtree.expect("/alist") + { let atom = self.objtree.expect("/atom"); if ty.is_subtype_of(&atom) { // Fine. @@ -1400,27 +1738,50 @@ impl<'o, 's> AnalyzeProc<'o, 's> { // Iffy conceptually, but the only detections on /tg/ are false positives in the // component system, where we loop over `var/datum/parent` that is known to be an // atom in a way that's hard for Dreamchecker to capture. - error(location, "iterating over a /datum which might not be an /atom") - .set_severity(Severity::Hint) - .register(self.context); + error( + location, + "iterating over a /datum which might not be an /atom", + ) + .set_severity(Severity::Hint) + .register(self.context); } else { // The type is a /datum/foo subtype that definitely can't be looped over. - error(location, format!("iterating over a {} which cannot be iterated", ty.path)) - .register(self.context); + error( + location, + format!( + "iterating over a {} which cannot be iterated", + ty.path + ), + ) + .register(self.context); } } - } + }, + StaticType::Proc => { + error( + location, + "iterating over a procpath which cannot be iterated".to_string(), + ) + .register(self.context); + }, } } if let Some(var_type) = var_type { self.visit_var(location, var_type, name, None, &mut scoped_locals); } - let mut state = self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals, false); state.end_loop(); - return state + return state; }, Statement::ForRange(for_range) => { - let ForRangeStatement { var_type, name, start, end, step, block } = &**for_range; + let ForRangeStatement { + var_type, + name, + start, + end, + step, + block, + } = &**for_range; let mut scoped_locals = local_vars.clone(); self.visit_expression(location, end, None, &mut scoped_locals); if let Some(step) = step { @@ -1429,21 +1790,24 @@ impl<'o, 's> AnalyzeProc<'o, 's> { if let Some(var_type) = var_type { self.visit_var(location, var_type, name, Some(start), &mut scoped_locals); } - let mut state = self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals, false); if let Some(startterm) = start.as_term() { if let Some(endterm) = end.as_term() { if let Some(validity) = startterm.valid_for_range(endterm, step.as_ref()) { if !validity { - error(location,"for range loop body is never reached due to invalid range") - .register(self.context); + error( + location, + "for range loop body is never reached due to invalid range", + ) + .register(self.context); } else { - return state + return state; } } } } state.end_loop(); - return state + return state; }, Statement::Var(var) => self.visit_var_stmt(location, var, local_vars), Statement::Vars(vars) => { @@ -1451,17 +1815,20 @@ impl<'o, 's> AnalyzeProc<'o, 's> { self.visit_var_stmt(location, each, local_vars); } }, - Statement::Setting { name, mode: SettingMode::Assign, value } => { + Statement::Setting { + name, + mode: SettingMode::Assign, + value, + } => { if name != "waitfor" { - return ControlFlow::allfalse() + return ControlFlow::allfalse(); } - match match value.as_term() { - Some(Term::Int(0)) => Some(true), - Some(Term::Ident(i)) if i == "FALSE" => Some(true), - _ => None, + if match value.as_term() { + Some(Term::Int(0)) => true, + Some(Term::Ident(i)) if i == "FALSE" => true, + _ => false, } { - Some(_) => { self.env.waitfor_procs.insert(self.proc_ref); }, - None => (), + self.env.waitfor_procs.insert(self.proc_ref); } }, Statement::Setting { .. } => {}, @@ -1471,78 +1838,226 @@ impl<'o, 's> AnalyzeProc<'o, 's> { if let Some(delay) = delay { self.visit_expression(location, delay, None, &mut scoped_locals); } - self.visit_block(block, &mut scoped_locals); + self.visit_block(block, &mut scoped_locals, false); self.inside_newcontext = self.inside_newcontext.wrapping_sub(1); }, - Statement::Switch { input, cases, default } => { + Statement::Switch { + input, + cases, + default, + } => { + check_switch_rand_range(input, cases, default, location, self.context); let mut allterm = ControlFlow::alltrue(); self.visit_control_condition(location, input); self.visit_expression(location, input, None, local_vars); for (case, ref block) in cases.iter() { let mut scoped_locals = local_vars.clone(); + if let [dm::ast::Case::Exact(Expression::BinaryOp { + op: BinaryOp::Or, .. + })] = case.elem[..] + { + error(case.location, "Elements in a switch-case branch separated by ||, this is likely in error and should be replaced by a comma") + .set_severity(Severity::Warning) + .register(self.context); + } for case_part in case.elem.iter() { match case_part { - dm::ast::Case::Exact(expr) => { self.visit_expression(case.location, expr, None, &mut scoped_locals); }, + dm::ast::Case::Exact(expr) => { + self.visit_expression( + case.location, + expr, + None, + &mut scoped_locals, + ); + }, dm::ast::Case::Range(start, end) => { - self.visit_expression(case.location, start, None, &mut scoped_locals); + self.visit_expression( + case.location, + start, + None, + &mut scoped_locals, + ); self.visit_expression(case.location, end, None, &mut scoped_locals); - } + }, } } - let state = self.visit_block(block, &mut scoped_locals); + let state = self.visit_block(block, &mut scoped_locals, false); allterm.merge_false(state); } if let Some(default) = default { - let state = self.visit_block(default, &mut local_vars.clone()); + let state = self.visit_block(default, &mut local_vars.clone(), false); allterm.merge_false(state); } else { allterm.no_else(); - return allterm + return allterm; } allterm.finalize(); - return allterm + return allterm; }, - Statement::TryCatch { try_block, catch_params, catch_block } => { - self.visit_block(try_block, &mut local_vars.clone()); + Statement::TryCatch { + try_block, + catch_params, + catch_block, + } => { + self.visit_block(try_block, &mut local_vars.clone(), false); if catch_params.len() > 1 { - error(location, format!("Expected 0 or 1 catch parameters, got {}", catch_params.len())) - .set_severity(Severity::Warning) - .register(self.context); + error( + location, + format!( + "Expected 0 or 1 catch parameters, got {}", + catch_params.len() + ), + ) + .set_severity(Severity::Warning) + .register(self.context); } let mut catch_locals = local_vars.clone(); for caught in catch_params.iter() { let (var_name, mut type_path) = match caught.split_last() { Some(x) => x, - None => continue + None => continue, }; match type_path.split_first() { Some((first, rest)) if first == "var" => type_path = rest, - _ => {} + _ => {}, } let var_type: VarType = type_path.iter().map(ToOwned::to_owned).collect(); self.visit_var(location, &var_type, var_name, None, &mut catch_locals); } - self.visit_block(catch_block, &mut catch_locals); + self.visit_block(catch_block, &mut catch_locals, false); + }, + Statement::Continue(_) => { + return ControlFlow { + returns: false, + continues: true, + breaks: false, + fuzzy: true, + } + }, + Statement::Break(_) => { + return ControlFlow { + returns: false, + continues: false, + breaks: true, + fuzzy: true, + } }, - Statement::Continue(_) => { return ControlFlow { returns: false, continues: true, breaks: false, fuzzy: true } }, - Statement::Break(_) => { return ControlFlow { returns: false, continues: false, breaks: true, fuzzy: true } }, Statement::Goto(_) => {}, - Statement::Label { name: _, block } => { self.visit_block(block, &mut local_vars.clone()); }, - Statement::Del(expr) => { self.visit_expression(location, expr, None, local_vars); }, + Statement::Label { name: _, block } => { + self.visit_block(block, &mut local_vars.clone(), false); + }, + Statement::Del(expr) => { + self.visit_expression(location, expr, None, local_vars); + }, + Statement::ForKeyValue(for_key_value) => { + let ForKeyValueStatement { + var_type, + key, + key_input_type: _, + value, + in_list, + block, + } = &**for_key_value; + let mut scoped_locals = local_vars.clone(); + if let Some(in_list) = in_list { + let list = self.visit_expression(location, in_list, None, &mut scoped_locals); + match list.static_ty { + StaticType::None => { + // Occurs extremely often due to DM not complaining about this, with + // over 800 detections on /tg/. Maybe a future lint. + }, + StaticType::List { .. } => { /* OK */ }, + StaticType::Type(ty) => { + if ty != self.objtree.expect("/world") + && ty != self.objtree.expect("/list") + && ty != self.objtree.expect("/alist") + { + let atom = self.objtree.expect("/atom"); + if ty.is_subtype_of(&atom) { + // Fine. + } else if atom.is_subtype_of(&ty) { + // Iffy conceptually, but the only detections on /tg/ are false positives in the + // component system, where we loop over `var/datum/parent` that is known to be an + // atom in a way that's hard for Dreamchecker to capture. + error( + location, + "iterating over a /datum which might not be an /atom", + ) + .set_severity(Severity::Hint) + .register(self.context); + } else { + // The type is a /datum/foo subtype that definitely can't be looped over. + error( + location, + format!( + "iterating over a {} which cannot be iterated", + ty.path + ), + ) + .register(self.context); + } + } + }, + StaticType::Proc => { + error( + location, + "iterating over a procpath which cannot be iterated".to_string(), + ) + .register(self.context); + }, + } + } + // This quite ugly but DM doesn't let you do for (var/k, var/v) + // only the type of the key is taken into account + if let Some(var_type) = var_type { + self.visit_var(location, var_type, key, None, &mut scoped_locals); + } + // the "v" in a DM for (var/k, v) statement is essentially typeless. + // There is currently no way to change that. + let var_type_value = VarType { + flags: VarTypeFlags::default(), + type_path: Box::new([]), + input_type: InputType::default(), + }; + self.visit_var(location, &var_type_value, value, None, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals, false); + state.end_loop(); + return state; + }, } - return ControlFlow::allfalse() + ControlFlow::allfalse() } - fn visit_var_stmt(&mut self, location: Location, var: &'o VarStatement, local_vars: &mut HashMap, RandomState>) { - self.visit_var(location, &var.var_type, &var.name, var.value.as_ref(), local_vars) + fn visit_var_stmt( + &mut self, + location: Location, + var: &'o VarStatement, + local_vars: &mut HashMap>, + ) { + self.visit_var( + location, + &var.var_type, + &var.name, + var.value.as_ref(), + local_vars, + ) } - fn visit_var(&mut self, location: Location, var_type: &VarType, name: &str, value: Option<&'o Expression>, local_vars: &mut HashMap, RandomState>) { + fn visit_var( + &mut self, + location: Location, + var_type: &VarType, + name: &str, + value: Option<&'o Expression>, + local_vars: &mut HashMap>, + ) { // Calculate type hint let static_type = self.env.static_type(location, &var_type.type_path); // Visit the expression if it's there let mut analysis = match value { - Some(ref expr) => self.visit_expression(location, expr, static_type.basic_type(), local_vars), + Some(expr) => { + self.visit_expression(location, expr, static_type.basic_type(), local_vars) + }, None => Analysis::null(), }; analysis.static_ty = static_type; @@ -1551,80 +2066,145 @@ impl<'o, 's> AnalyzeProc<'o, 's> { local_vars.insert(name.to_owned(), LocalVar { location, analysis }); } - fn visit_expression(&mut self, location: Location, expression: &'o Expression, type_hint: Option>, local_vars: &mut HashMap, RandomState>) -> Analysis<'o> { + fn visit_expression( + &mut self, + location: Location, + expression: &'o Expression, + type_hint: Option>, + local_vars: &mut HashMap>, + ) -> Analysis<'o> { match expression { Expression::Base { term, follow } => { - let base_type_hint = if follow.is_empty() { - type_hint - } else { - None - }; + let base_type_hint = if follow.is_empty() { type_hint } else { None }; let mut ty = self.visit_term(term.location, &term.elem, base_type_hint, local_vars); for each in follow.iter() { ty = self.visit_follow(each.location, ty, &each.elem, local_vars); } ty }, - Expression::BinaryOp { op: BinaryOp::LShift, lhs, rhs } => { + Expression::BinaryOp { + op: BinaryOp::LShift, + lhs, + rhs, + } => { let lty = self.visit_expression(location, lhs, None, local_vars); if lty.static_ty == StaticType::Type(self.objtree.expect("/mob")) { - self.env.impure_procs.insert_violator(self.proc_ref, "LShift onto mob", location); + self.env.impure_procs.insert_violator( + self.proc_ref, + "LShift onto mob", + location, + ); } else if lty.static_ty == StaticType::Type(self.objtree.expect("/savefile")) { - self.env.impure_procs.insert_violator(self.proc_ref, "LShift onto savefile", location); + self.env.impure_procs.insert_violator( + self.proc_ref, + "LShift onto savefile", + location, + ); } else if lty.static_ty == StaticType::Type(self.objtree.expect("/list")) { - self.env.impure_procs.insert_violator(self.proc_ref, "LShift onto list", location); + self.env.impure_procs.insert_violator( + self.proc_ref, + "LShift onto list", + location, + ); } let rty = self.visit_expression(location, rhs, None, local_vars); self.visit_binary(lty, rty, BinaryOp::LShift) }, - Expression::BinaryOp { op: BinaryOp::In, lhs, rhs } => { + Expression::BinaryOp { + op: BinaryOp::In, + lhs, + rhs, + } => { // check for incorrect/ambiguous in statements match &**lhs { Expression::Base { term, follow } => { for each in follow.iter() { if let Follow::Unary(unary) = each.elem { - error(location, format!("ambiguous `{}` on left side of an `in`", unary.name())) - .set_severity(Severity::Warning) - .with_errortype("ambiguous_in_lhs") - .with_note(location, format!("add parentheses to fix: `{}`", unary.around("(a in b)"))) - .with_note(location, format!("add parentheses to disambiguate: `({}) in b`", unary.around("a"))) - .register(self.context); + error( + location, + format!("ambiguous `{}` on left side of an `in`", unary.name()), + ) + .set_severity(Severity::Warning) + .with_errortype("ambiguous_in_lhs") + .with_note( + location, + format!( + "add parentheses to fix: `{}`", + unary.around("(a in b)") + ), + ) + .with_note( + location, + format!( + "add parentheses to disambiguate: `({}) in b`", + unary.around("a") + ), + ) + .register(self.context); break; } } }, Expression::BinaryOp { op, lhs, rhs } => { - error(location, format!("ambiguous `{}` on left side of an `in`", op)) - .set_severity(Severity::Warning) - .with_errortype("ambiguous_in_lhs") - .with_note(location, format!("add parentheses to fix: `a {} (b in c)`", op)) - .with_note(location, format!("add parentheses to disambiguate: `(a {} b) in c`", op)) - .register(self.context); + error( + location, + format!("ambiguous `{op}` on left side of an `in`"), + ) + .set_severity(Severity::Warning) + .with_errortype("ambiguous_in_lhs") + .with_note( + location, + format!("add parentheses to fix: `a {op} (b in c)`"), + ) + .with_note( + location, + format!("add parentheses to disambiguate: `(a {op} b) in c`"), + ) + .register(self.context); }, Expression::AssignOp { op, lhs, rhs } => { - error(location, format!("ambiguous `{}` on left side of an `in`", op)) - .set_severity(Severity::Warning) - .with_errortype("ambiguous_in_lhs") - .with_note(location, format!("add parentheses to fix: `a {} (b in c)`", op)) - .with_note(location, format!("add parentheses to disambiguate: `(a {} b) in c`", op)) - .register(self.context); + error( + location, + format!("ambiguous `{op}` on left side of an `in`"), + ) + .set_severity(Severity::Warning) + .with_errortype("ambiguous_in_lhs") + .with_note( + location, + format!("add parentheses to fix: `a {op} (b in c)`"), + ) + .with_note( + location, + format!("add parentheses to disambiguate: `(a {op} b) in c`"), + ) + .register(self.context); }, Expression::TernaryOp { cond, if_, else_ } => { - error(location, format!("ambiguous ternary on left side of an `in`")) - .set_severity(Severity::Warning) - .with_errortype("ambiguous_in_lhs") - .with_note(location, "add parentheses to fix: `a ? b : (c in d)`") - .with_note(location, "add parentheses to disambiguate: `(a ? b : c) in d`") - .register(self.context); + error( + location, + "ambiguous ternary on left side of an `in`".to_string(), + ) + .set_severity(Severity::Warning) + .with_errortype("ambiguous_in_lhs") + .with_note(location, "add parentheses to fix: `a ? b : (c in d)`") + .with_note( + location, + "add parentheses to disambiguate: `(a ? b : c) in d`", + ) + .register(self.context); }, }; let lty = self.visit_expression(location, lhs, None, local_vars); let rty = self.visit_expression(location, rhs, None, local_vars); self.visit_binary(lty, rty, BinaryOp::In) }, - Expression::BinaryOp { op: BinaryOp::Or, lhs, rhs } => { + Expression::BinaryOp { + op: BinaryOp::Or, + lhs, + rhs, + } => { // It appears that DM does this in more cases than this, but // this is the only case I've seen it used in the wild. // ex: var/datum/cache_entry/E = cache[key] || new @@ -1636,17 +2216,27 @@ impl<'o, 's> AnalyzeProc<'o, 's> { let lty = self.visit_expression(location, lhs, None, local_vars); let rty = self.visit_expression(location, rhs, None, local_vars); match op { - BinaryOp::BitAnd => self.check_negated_bitwise(lhs, location, BinaryOp::BitAnd, BinaryOp::And), - BinaryOp::BitOr => self.check_negated_bitwise(lhs, location, BinaryOp::BitOr, BinaryOp::Or), - BinaryOp::BitXor => self.check_negated_bitwise(lhs, location, BinaryOp::BitXor, BinaryOp::NotEq), - _ => {} + BinaryOp::BitAnd => { + self.check_negated_bitwise(lhs, location, BinaryOp::BitAnd, BinaryOp::And) + }, + BinaryOp::BitOr => { + self.check_negated_bitwise(lhs, location, BinaryOp::BitOr, BinaryOp::Or) + }, + BinaryOp::BitXor => { + self.check_negated_bitwise(lhs, location, BinaryOp::BitXor, BinaryOp::NotEq) + }, + _ => {}, } self.visit_binary(lty, rty, *op) }, Expression::AssignOp { lhs, rhs, .. } => { let lhs = self.visit_expression(location, lhs, None, local_vars); if let Some(true) = lhs.is_impure { - self.env.impure_procs.insert_violator(self.proc_ref, "Assignment on purity breaking expression", location); + self.env.impure_procs.insert_violator( + self.proc_ref, + "Assignment on purity breaking expression", + location, + ); } self.visit_expression(location, rhs, lhs.static_ty.basic_type(), local_vars) }, @@ -1656,32 +2246,82 @@ impl<'o, 's> AnalyzeProc<'o, 's> { let ty = self.visit_expression(location, if_, type_hint, local_vars); self.visit_expression(location, else_, type_hint, local_vars); ty - } + }, } } - fn visit_term(&mut self, location: Location, term: &'o Term, type_hint: Option>, local_vars: &mut HashMap, RandomState>) -> Analysis<'o> { + fn visit_term( + &mut self, + location: Location, + term: &'o Term, + type_hint: Option>, + local_vars: &mut HashMap>, + ) -> Analysis<'o> { match term { Term::Null => Analysis::null(), - Term::Int(number) => Analysis::from_value(self.objtree, Constant::from(*number), type_hint), - Term::Float(number) => Analysis::from_value(self.objtree, Constant::from(*number), type_hint), - Term::String(text) => Analysis::from_value(self.objtree, Constant::String(text.as_str().into()), type_hint), - Term::Resource(text) => Analysis::from_value(self.objtree, Constant::Resource(text.as_str().into()), type_hint), + Term::Int(number) => { + Analysis::from_value(self.objtree, Constant::from(*number), type_hint) + }, + Term::Float(number) => { + Analysis::from_value(self.objtree, Constant::from(*number), type_hint) + }, + Term::String(text) => Analysis::from_value( + self.objtree, + Constant::String(text.as_str().into()), + type_hint, + ), + Term::Resource(text) => Analysis::from_value( + self.objtree, + Constant::Resource(text.as_str().into()), + type_hint, + ), Term::As(_) => assumption_set![Assumption::IsNum(true)].into(), Term::Ident(unscoped_name) => { if let Some(var) = local_vars.get(unscoped_name) { - return var.analysis.clone() + var.analysis + .clone() .with_fix_hint(var.location, "add additional type info here") - } - if let Some(decl) = self.ty.get_var_declaration(unscoped_name) { - //println!("found type var"); - let mut ana = self.static_type(location, &decl.var_type.type_path) + } else if unscoped_name == "type" { + // Strictly speaking "type" might be any subset of our current type, but let's return something useful + // so that `nameof(type::foo)` is sensible. + let ty = self.ty; + let pop = dm::constants::Pop::from( + ty.path + .split('/') + .skip(1) + .map(ToOwned::to_owned) + .collect::>() + .into_boxed_slice(), + ); + Analysis { + static_ty: StaticType::None, + aset: assumption_set![Assumption::IsPath(true, ty)], + value: Some(Constant::Prefab(Box::new(pop))), + fix_hint: None, + is_impure: None, + } + } else if let Some(decl) = self.ty.get_var_declaration(unscoped_name) { + let mut ana = self + .static_type(location, &decl.var_type.type_path) .with_fix_hint(decl.location, "add additional type info here"); ana.is_impure = Some(true); - return ana + ana } else { - error(location, format!("undefined var: {:?}", unscoped_name)) + error(location, format!("undefined var: {unscoped_name:?}")) + .register(self.context); + Analysis::empty() + } + }, + Term::GlobalIdent(global_name) => { + if let Some(decl) = self.objtree.root().get_var_declaration(global_name) { + let mut ana = self + .static_type(location, &decl.var_type.type_path) + .with_fix_hint(decl.location, "add additional type info here"); + ana.is_impure = Some(true); + ana + } else { + error(location, format!("undefined global var: {global_name:?}")) .register(self.context); Analysis::empty() } @@ -1690,18 +2330,28 @@ impl<'o, 's> AnalyzeProc<'o, 's> { Term::Expr(expr) => self.visit_expression(location, expr, type_hint, local_vars), Term::Prefab(prefab) => { if let Some(nav) = self.ty.navigate_path(&prefab.path) { - let ty = nav.ty(); // TODO: handle proc/verb paths here - let pop = dm::constants::Pop::from(ty.path.split("/").skip(1).map(ToOwned::to_owned).collect::>().into_boxed_slice()); + let ty = nav.ty(); // TODO: handle proc/verb paths here + let pop = dm::constants::Pop::from( + ty.path + .split('/') + .skip(1) + .map(ToOwned::to_owned) + .collect::>() + .into_boxed_slice(), + ); Analysis { static_ty: StaticType::None, - aset: assumption_set![Assumption::IsPath(true, nav.ty())].into(), + aset: assumption_set![Assumption::IsPath(true, nav.ty())], value: Some(Constant::Prefab(Box::new(pop))), fix_hint: None, is_impure: None, } } else { - error(location, format!("failed to resolve path {}", FormatTypePath(&prefab.path))) - .register(self.context); + error( + location, + format!("failed to resolve path {}", FormatTypePath(&prefab.path)), + ) + .register(self.context); Analysis::empty() } }, @@ -1715,18 +2365,22 @@ impl<'o, 's> AnalyzeProc<'o, 's> { }, Term::Call(unscoped_name, args) => { - if self.inside_newcontext == 0 && matches!(unscoped_name.as_str(), - "sleep" - | "alert" - | "shell" - | "winexists" - | "winget") { - self.env.sleeping_procs.insert_violator(self.proc_ref, unscoped_name, location); + if self.inside_newcontext == 0 + && matches!( + unscoped_name.as_str(), + "sleep" | "alert" | "shell" | "winexists" | "winget" + ) + { + self.env + .sleeping_procs + .insert_violator(self.proc_ref, unscoped_name, location); } self.check_type_sleepers(self.ty, location, unscoped_name); let src = self.ty; if let Some(proc) = self.ty.get_proc(unscoped_name) { self.visit_call(location, src, proc, args, false, local_vars) + } else if unscoped_name == "__PROC__" { + self.visit_call(location, src, self.proc_ref, args, false, local_vars) } else if unscoped_name == "SpacemanDMM_unlint" { // Escape hatch for cases like `src` in macros used in // global procs. @@ -1734,12 +2388,18 @@ impl<'o, 's> AnalyzeProc<'o, 's> { } else if unscoped_name == "SpacemanDMM_debug" { eprintln!("SpacemanDMM_debug:"); for arg in args.iter() { - eprintln!(" {:?}", self.visit_expression(location, arg, None, local_vars)); + eprintln!( + " {:?}", + self.visit_expression(location, arg, None, local_vars) + ); } Analysis::empty() } else { - error(location, format!("undefined proc: {:?} on {}", unscoped_name, self.ty)) - .register(self.context); + error( + location, + format!("undefined proc: {:?} on {}", unscoped_name, self.ty), + ) + .register(self.context); Analysis::empty() } }, @@ -1763,6 +2423,15 @@ impl<'o, 's> AnalyzeProc<'o, 's> { Analysis::empty() } }, + Term::GlobalCall(global_name, args) => { + if let Some(proc) = self.objtree.root().get_proc(global_name) { + self.visit_call(location, self.objtree.root(), proc, args, true, local_vars) + } else { + error(location, format!("undefined global proc: {global_name:?}")) + .register(self.context); + Analysis::empty() + } + }, Term::NewImplicit { args } => { if let Some(hint) = type_hint { @@ -1779,9 +2448,12 @@ impl<'o, 's> AnalyzeProc<'o, 's> { // TODO: handle proc/verb paths here self.visit_new(location, nav.ty(), args, local_vars) } else { - error(location, format!("failed to resolve path {}", FormatTypePath(&prefab.path))) - .register(self.context); - Analysis::empty() + error( + location, + format!("failed to resolve path {}", FormatTypePath(&prefab.path)), + ) + .register(self.context); + Analysis::empty() } }, Term::NewMiniExpr { .. } => { @@ -1793,9 +2465,15 @@ impl<'o, 's> AnalyzeProc<'o, 's> { self.visit_arguments(location, args, local_vars); Analysis::from_static_type(self.objtree.expect("/list")) }, - Term::Input { args, input_type, in_list } => { + Term::Input { + args, + input_type, + in_list, + } => { if self.inside_newcontext == 0 { - self.env.sleeping_procs.insert_violator(self.proc_ref, "input", location); + self.env + .sleeping_procs + .insert_violator(self.proc_ref, "input", location); } // TODO: deal with in_list self.visit_arguments(location, args, local_vars); @@ -1815,7 +2493,13 @@ impl<'o, 's> AnalyzeProc<'o, 's> { assumption_set![Assumption::IsType(true, self.objtree.expect("/area"))].into() } else if without_null == InputType::TURF { assumption_set![Assumption::IsType(true, self.objtree.expect("/turf"))].into() - } else if without_null == InputType::TEXT || without_null == InputType::MESSAGE || without_null == InputType::KEY || without_null == InputType::PASSWORD || without_null == InputType::COLOR || without_null.is_empty() { + } else if without_null == InputType::TEXT + || without_null == InputType::MESSAGE + || without_null == InputType::KEY + || without_null == InputType::PASSWORD + || without_null == InputType::COLOR + || without_null.is_empty() + { assumption_set![Assumption::IsText(true)].into() } else if without_null == InputType::NUM { assumption_set![Assumption::IsNum(true)].into() @@ -1834,7 +2518,8 @@ impl<'o, 's> AnalyzeProc<'o, 's> { self.visit_expression(location, expr, None, local_vars); } - if args.len() == 3 { // X,Y,Z - it's gotta be a turf + if args.len() == 3 { + // X,Y,Z - it's gotta be a turf assumption_set![Assumption::IsType(true, self.objtree.expect("/turf"))].into() } else { Analysis::empty() @@ -1854,12 +2539,74 @@ impl<'o, 's> AnalyzeProc<'o, 's> { Term::DynamicCall(lhs_args, rhs_args) => { self.visit_arguments(location, lhs_args, local_vars); self.visit_arguments(location, rhs_args, local_vars); - Analysis::empty() // TODO + Analysis::empty() // TODO + }, + Term::ExternalCall { + library, + function, + args, + } => { + if let Some(library) = library { + self.visit_expression(location, library, None, local_vars); + } + self.visit_expression(location, function, None, local_vars); + self.visit_arguments(location, args, local_vars); + Analysis::empty() // TODO + }, + + Term::__TYPE__ => { + let pop = dm::constants::Pop::from( + self.ty + .path + .split('/') + .skip(1) + .map(ToOwned::to_owned) + .collect::>() + .into_boxed_slice(), + ); + Analysis { + static_ty: StaticType::None, + aset: assumption_set![Assumption::IsPath(true, self.ty)], + value: Some(Constant::Prefab(Box::new(pop))), + fix_hint: None, + is_impure: None, + } + }, + Term::__PROC__ => { + // Can't fuckin do it bros + Analysis::empty() + }, + Term::__IMPLIED_TYPE__ => { + let Some(implied_type) = type_hint else { + return Analysis::empty(); + }; + let pop = dm::constants::Pop::from( + implied_type + .path + .split('/') + .skip(1) + .map(ToOwned::to_owned) + .collect::>() + .into_boxed_slice(), + ); + Analysis { + static_ty: StaticType::None, + aset: assumption_set![Assumption::IsPath(true, self.ty)], + value: Some(Constant::Prefab(Box::new(pop))), + fix_hint: None, + is_impure: None, + } }, } } - fn visit_new(&mut self, location: Location, typepath: TypeRef<'o>, args: &'o Option>, local_vars: &mut HashMap, RandomState>) -> Analysis<'o> { + fn visit_new( + &mut self, + location: Location, + typepath: TypeRef<'o>, + args: &'o Option>, + local_vars: &mut HashMap>, + ) -> Analysis<'o> { if let Some(new_proc) = typepath.get_proc("New") { self.visit_call( location, @@ -1869,45 +2616,73 @@ impl<'o, 's> AnalyzeProc<'o, 's> { // New calls are exact: `new /datum()` will always call // `/datum/New()` and never an override. true, - local_vars); - } else if typepath.path != "/list" { - error(location, format!("couldn't find {}/proc/New", typepath.path)) - .register(self.context); + local_vars, + ); + } else if typepath.path != "/list" + && typepath.path != "/alist" + && typepath.path != "/vector" + && typepath.path != "/pixloc" + { + error( + location, + format!("couldn't find {}/proc/New", typepath.path), + ) + .register(self.context); } assumption_set![Assumption::IsType(true, typepath)].into() } fn check_type_sleepers(&mut self, ty: TypeRef<'o>, location: Location, unscoped_name: &str) { match ty.get().path.as_str() { - "/client" => if self.inside_newcontext == 0 && matches!(unscoped_name, - "SoundQuery" - | "MeasureText") { - self.env.sleeping_procs.insert_violator(self.proc_ref, format!("client.{}", unscoped_name).as_str(), location); + "/client" => { + if self.inside_newcontext == 0 + && matches!(unscoped_name, "SoundQuery" | "MeasureText") + { + self.env.sleeping_procs.insert_violator( + self.proc_ref, + format!("client.{unscoped_name}").as_str(), + location, + ); + } }, - "/world" => if self.inside_newcontext == 0 && matches!(unscoped_name, - "Import" - | "Export") { - self.env.sleeping_procs.insert_violator(self.proc_ref, format!("world.{}", unscoped_name).as_str(), location); + "/world" => { + if self.inside_newcontext == 0 && matches!(unscoped_name, "Import" | "Export") { + self.env.sleeping_procs.insert_violator( + self.proc_ref, + format!("world.{unscoped_name}").as_str(), + location, + ); + } }, _ => {}, } } - fn visit_follow(&mut self, location: Location, lhs: Analysis<'o>, rhs: &'o Follow, local_vars: &mut HashMap, RandomState>) -> Analysis<'o> { + fn visit_follow( + &mut self, + location: Location, + lhs: Analysis<'o>, + rhs: &'o Follow, + local_vars: &mut HashMap>, + ) -> Analysis<'o> { match rhs { Follow::Unary(op) => self.visit_unary(lhs, op, location, local_vars), Follow::Field(PropertyAccessKind::Colon, _) => Analysis::empty(), Follow::Field(PropertyAccessKind::SafeColon, _) => Analysis::empty(), - Follow::Call(PropertyAccessKind::Colon, _, args) | - Follow::Call(PropertyAccessKind::SafeColon, _, args) => { + Follow::Call(PropertyAccessKind::Colon, _, args) + | Follow::Call(PropertyAccessKind::SafeColon, _, args) => { // No analysis yet, but be sure to visit the arguments for arg in args.iter() { let mut argument_value = arg; - if let Expression::AssignOp { op: AssignOp::Assign, lhs, rhs } = arg { + if let Expression::AssignOp { + op: AssignOp::Assign, + lhs, + rhs, + } = arg + { match lhs.as_term() { - Some(Term::Ident(name)) | - Some(Term::String(name)) => { + Some(Term::Ident(name)) | Some(Term::String(name)) => { // Don't visit_expression the kwarg key. argument_value = rhs; }, @@ -1926,113 +2701,285 @@ impl<'o, 's> AnalyzeProc<'o, 's> { StaticType::List { keys, .. } => { let mut res = Analysis::from(*keys); if let Some((loc, _)) = lhs.fix_hint { - res.fix_hint = Some((loc, "add a type annotation after /list here".to_owned())) + res.fix_hint = + Some((loc, "add a type annotation after /list here".to_owned())) } res }, - _ => lhs.clone() // carry through fix_hint + StaticType::Type(typeref) => { + if typeref.get_proc("operator[]").is_none() { + error(location, format!("invalid list access on {}", typeref.path)) + .with_errortype("improper_index") + .register(self.context); + } + lhs.clone() + }, + _ => lhs.clone(), // carry through fix_hint } }, Follow::Field(kind, name) => { - if let Some(ty) = lhs.static_ty.basic_type() { - if let Some(decl) = ty.get_var_declaration(name) { + if let StaticType::Proc = lhs.static_ty { + match name.as_str() { + "type" | "name" | "desc" | "category" | "invisibility" => {}, + _ => { + error(location, format!("undefined field: {name:?} on procpath")) + .register(self.context); + }, + } + Analysis::empty() + } else if let Some(ty) = lhs.static_ty.basic_type() { + if ty.path == "/callee" && name == "proc" { + // Special cased for now because this might be the only place it appears? + // Or maybe we should also handle new procpath() returning a procpath. + Analysis::from(StaticType::Proc) + } else if let Some(decl) = ty.get_var_declaration(name) { if ty != self.ty && decl.var_type.flags.is_private() { - error(location, format!("field {:?} on {} is declared as private", name, ty)) - .with_errortype("private_var") - .set_severity(Severity::Warning) - .with_note(decl.location, "definition is here") - .register(self.context); - } else if !self.ty.is_subtype_of(ty.get()) && decl.var_type.flags.is_protected() { - error(location, format!("field {:?} on {} is declared as protected", name, ty)) - .with_errortype("protected_var") - .set_severity(Severity::Warning) - .with_note(decl.location, "definition is here") - .register(self.context); + error( + location, + format!("field {name:?} on {ty} is declared as private"), + ) + .with_errortype("private_var") + .set_severity(Severity::Warning) + .with_note(decl.location, "definition is here") + .register(self.context); + } else if !self.ty.is_subtype_of(ty.get()) + && decl.var_type.flags.is_protected() + { + error( + location, + format!("field {name:?} on {ty} is declared as protected"), + ) + .with_errortype("protected_var") + .set_severity(Severity::Warning) + .with_note(decl.location, "definition is here") + .register(self.context); } self.static_type(location, &decl.var_type.type_path) .with_fix_hint(decl.location, "add additional type info here") } else { - error(location, format!("undefined field: {:?} on {}", name, ty)) + error(location, format!("undefined field: {name:?} on {ty}")) .register(self.context); Analysis::empty() } } else { - error(location, format!("field access requires static type: {:?}", name)) - .set_severity(Severity::Warning) - .with_errortype("field_access_static_type") - .with_fix_hint(&lhs) - .register(self.context); + error( + location, + format!("field access requires static type: {name:?}"), + ) + .set_severity(Severity::Warning) + .with_errortype("field_access_static_type") + .with_fix_hint(&lhs) + .register(self.context); Analysis::empty() } }, + Follow::StaticField(name) => { + let real_type = match lhs.static_ty.basic_type() { + Some(existing_type) => existing_type, + None => { + let Some(to_our_side) = lhs.value else { + error(location, format!("no typepath found for: {name:?}")) + .register(self.context); + return Analysis::empty(); + }; + let Constant::Prefab(typepop) = to_our_side else { + error(location, format!("static access requires a static typepath, {to_our_side} found instead")) + .register(self.context); + return Analysis::empty(); + }; + if !typepop.vars.is_empty() { + error(location, format!("static access requires a static typepath, {typepop} found instead")) + .register(self.context); + return Analysis::empty(); + } + let typepath = dm::ast::FormatTreePath(&typepop.path).to_string(); + let Some(found_type) = self.objtree.find(typepath.as_str()) else { + error(location, format!("static access requires an existing typepath, {typepath} found instead")) + .register(self.context); + return Analysis::empty(); + }; + found_type + }, + }; + let Some(decl) = real_type.get_var_declaration(name) else { + error( + location, + format!("undefined field: {name:?} on {real_type}"), + ) + .register(self.context); + return Analysis::empty(); + }; + + self.static_type(location, &decl.var_type.type_path) + .with_fix_hint(decl.location, "add additional type info here") + }, + Follow::Call(kind, name, arguments) => { if let Some(ty) = lhs.static_ty.basic_type() { self.check_type_sleepers(ty, location, name); if let Some(proc) = ty.get_proc(name) { - if let Some((privateproc, true, decllocation)) = self.env.private.get_self_or_parent(proc) { + if let Some((privateproc, true, decllocation)) = + self.env.private.get_self_or_parent(proc) + { if ty != privateproc.ty() { - error(location, format!("{} attempting to call private proc {}, types do not match", self.proc_ref, privateproc)) - .with_errortype("private_proc") - .with_note(decllocation, "prohibited by this private_proc annotation") - .register(self.context); - return Analysis::empty() // dont double up with visit_call() + error( + location, + format!( + "{} attempting to call private proc {}, types do not match", + self.proc_ref, privateproc + ), + ) + .with_errortype("private_proc") + .with_note( + decllocation, + "prohibited by this private_proc annotation", + ) + .register(self.context); + return Analysis::empty(); // dont double up with visit_call() } } - if let Some((protectedproc, true, decllocation)) = self.env.protected.get_self_or_parent(proc) { + if let Some((protectedproc, true, decllocation)) = + self.env.protected.get_self_or_parent(proc) + { if !self.ty.is_subtype_of(protectedproc.ty().get()) { - error(location, format!("{} attempting to call protected proc {}", self.proc_ref, protectedproc)) - .with_errortype("protected_proc") - .with_note(decllocation, "prohibited by this protected_proc annotation") - .register(self.context); + error( + location, + format!( + "{} attempting to call protected proc {}", + self.proc_ref, protectedproc + ), + ) + .with_errortype("protected_proc") + .with_note( + decllocation, + "prohibited by this protected_proc annotation", + ) + .register(self.context); } } self.visit_call(location, ty, proc, arguments, false, local_vars) } else { - error(location, format!("undefined proc: {:?} on {}", name, ty)) + error(location, format!("undefined proc: {name:?} on {ty}")) .register(self.context); Analysis::empty() } } else { - error(location, format!("proc call requires static type: {:?}", name)) - .set_severity(Severity::Warning) - .with_errortype("proc_call_static_type") - .with_fix_hint(&lhs) - .register(self.context); + error( + location, + format!("proc call requires static type: {name:?}"), + ) + .set_severity(Severity::Warning) + .with_errortype("proc_call_static_type") + .with_fix_hint(&lhs) + .register(self.context); Analysis::empty() } }, + Follow::ProcReference(name) => { + let real_type = match lhs.static_ty.basic_type() { + Some(existing_type) => existing_type, + None => { + let Some(to_our_side) = lhs.value else { + error(location, format!("no typepath found for: {name:?}")) + .register(self.context); + return Analysis::empty(); + }; + let Constant::Prefab(typepop) = to_our_side else { + error(location, format!("static proc reference requires a static typepath, {to_our_side} found instead")) + .register(self.context); + return Analysis::empty(); + }; + if !&typepop.vars.is_empty() { + error(location, format!("static proc reference requires a static typepath, {typepop} found instead")) + .register(self.context); + return Analysis::empty(); + } + let typepath = dm::ast::FormatTreePath(&typepop.path).to_string(); + let Some(found_type) = self.objtree.find(typepath.as_str()) else { + error(location, format!("static proc reference requires an existing typepath, {typepath} found instead")) + .register(self.context); + return Analysis::empty(); + }; + found_type + }, + }; + let Some(decl) = real_type.get_proc(name) else { + error(location, format!("undefined proc: {name:?} on {real_type}")) + .register(self.context); + return Analysis::empty(); + }; + + // Gonna build the proc's path + let mut path_elements: Vec = real_type + .get() + .path + .split('/') + .filter(|elem| !elem.is_empty()) + .map(|segment| segment.to_string()) + .collect(); + // Only tricky bit is adding on the type if required + if let Some(declaration) = decl.get_declaration() { + path_elements.push(declaration.kind.name().to_string()); + } + path_elements.push(decl.name().to_string()); + let path_const = dm::constants::Pop::from(path_elements.into_boxed_slice()); + Analysis { + static_ty: StaticType::None, + aset: assumption_set![Assumption::IsPath(true, real_type)], + value: Some(Constant::Prefab(Box::new(path_const))), + fix_hint: None, + is_impure: None, + } + }, } } // checks operatorX overloads on types - fn check_operator_overload(&mut self, rhs: Analysis<'o>, location: Location, operator: &str, local_vars: &mut HashMap, RandomState>) -> Analysis<'o> { + fn check_operator_overload( + &mut self, + rhs: Analysis<'o>, + location: Location, + operator: &str, + local_vars: &mut HashMap>, + ) -> Analysis<'o> { if let Some(impurity) = rhs.is_impure { if impurity { - self.env.impure_procs.insert_violator(self.proc_ref, &format!("{} done on non-local var", operator), location); + self.env.impure_procs.insert_violator( + self.proc_ref, + &format!("{operator} done on non-local var"), + location, + ); } } - let typeerror; - match rhs.static_ty { - StaticType::None => { - return Analysis::empty() - }, + let typeerror = match rhs.static_ty { + StaticType::None => return Analysis::empty(), StaticType::Type(typeref) => { // Its been overloaded, assume they really know they want to do this if let Some(proc) = typeref.get_proc(operator) { - return self.visit_call(location, typeref, proc, &[], true, local_vars) + return self.visit_call(location, typeref, proc, &[], true, local_vars); } - typeerror = typeref.get().pretty_path(); + typeref.get().pretty_path() }, StaticType::List { list, .. } => { - typeerror = "list"; + "list" }, + StaticType::Proc => return Analysis::empty(), }; - error(location, format!("Attempting {} on a {} which does not overload {}", operator, typeerror, operator)) - .register(self.context); - return Analysis::empty() + error( + location, + format!("Attempting {operator} on a {typeerror} which does not overload {operator}"), + ) + .register(self.context); + Analysis::empty() } - fn visit_unary(&mut self, rhs: Analysis<'o>, op: &UnaryOp, location: Location, local_vars: &mut HashMap, RandomState>) -> Analysis<'o> { + fn visit_unary( + &mut self, + rhs: Analysis<'o>, + op: &UnaryOp, + location: Location, + local_vars: &mut HashMap>, + ) -> Analysis<'o> { match op { // !x just evaluates the "truthiness" of x and negates it, returning 1 or 0 UnaryOp::Not => Analysis::from(assumption_set![Assumption::IsNum(true)]), @@ -2051,17 +2998,30 @@ impl<'o, 's> AnalyzeProc<'o, 's> { } // checks for bitwise operations on a negated LHS - fn check_negated_bitwise(&mut self, lhs: &dm::ast::Expression, location: Location, bit_op: BinaryOp, bool_op: BinaryOp) { - guard!(let Expression::Base { follow, .. } = lhs else { return }); - let any_not = follow.iter().any(|f| matches!(f.elem, Follow::Unary(UnaryOp::Not))); + fn check_negated_bitwise( + &mut self, + lhs: &dm::ast::Expression, + location: Location, + bit_op: BinaryOp, + bool_op: BinaryOp, + ) { + let Expression::Base { follow, .. } = lhs else { + return; + }; + let any_not = follow + .iter() + .any(|f| matches!(f.elem, Follow::Unary(UnaryOp::Not))); if any_not { - error(location, format!("Ambiguous `!` on left side of bitwise `{}` operator", bit_op)) - .with_errortype("ambiguous_not_bitwise") - .set_severity(Severity::Warning) - .with_note(location, format!("Did you mean `!(x {} y)`?", bit_op)) - .with_note(location, format!("Did you mean `!x {} y`?", bool_op)) - .with_note(location, format!("Did you mean `~x {} y`?", bit_op)) - .register(self.context); + error( + location, + format!("Ambiguous `!` on left side of bitwise `{bit_op}` operator"), + ) + .with_errortype("ambiguous_not_bitwise") + .set_severity(Severity::Warning) + .with_note(location, format!("Did you mean `!(x {bit_op} y)`?")) + .with_note(location, format!("Did you mean `!x {bool_op} y`?")) + .with_note(location, format!("Did you mean `~x {bit_op} y`?")) + .register(self.context); } } @@ -2070,77 +3030,129 @@ impl<'o, 's> AnalyzeProc<'o, 's> { if lhs.static_ty.is_list() { // If the LHS of these operators is a list, so is the result. match op { - BinaryOp::Add | - BinaryOp::Sub | - BinaryOp::BitOr | - BinaryOp::BitAnd | - BinaryOp::BitXor => return lhs.static_ty.into(), - _ => {} + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::BitOr + | BinaryOp::BitAnd + | BinaryOp::BitXor => return lhs.static_ty.into(), + _ => {}, } } Analysis::empty() } - fn check_filter_flag(&mut self, expr: &'o Expression, can_be_zero: bool, location: Location, typevalue: &str, valid_flags: &[&str], flagfieldname: &str, exclusive: bool) { + // It's fine. + #[allow(clippy::too_many_arguments)] + fn check_filter_flag( + &mut self, + expr: &'o Expression, + can_be_zero: bool, + location: Location, + typevalue: &str, + valid_flags: &[&str], + flagfieldname: &str, + exclusive: bool, + ) { match expr { - Expression::BinaryOp{ op: BinaryOp::BitOr, lhs, rhs } => { + Expression::BinaryOp { + op: BinaryOp::BitOr, + lhs, + rhs, + } => { if exclusive { - error(location, format!("filter(type=\"{}\") '{}' parameter must have one value, found bitwise OR", typevalue, flagfieldname)) + error(location, format!("filter(type=\"{typevalue}\") '{flagfieldname}' parameter must have one value, found bitwise OR")) .with_filter_args(location, typevalue) .register(self.context); - return + return; } // recurse - self.check_filter_flag(lhs, can_be_zero, location, typevalue, valid_flags, flagfieldname, exclusive); - self.check_filter_flag(rhs, can_be_zero, location, typevalue, valid_flags, flagfieldname, exclusive); + self.check_filter_flag( + lhs, + can_be_zero, + location, + typevalue, + valid_flags, + flagfieldname, + exclusive, + ); + self.check_filter_flag( + rhs, + can_be_zero, + location, + typevalue, + valid_flags, + flagfieldname, + exclusive, + ); }, - Expression::Base{ term, follow } => { - if follow.len() > 0 { - error(location, "filter() flag fields cannot have unary ops or field accesses") - .register(self.context); - return + Expression::Base { term, follow } => { + if !follow.is_empty() { + error( + location, + "filter() flag fields cannot have unary ops or field accesses", + ) + .register(self.context); + return; } match &term.elem { Term::Ident(flagname) => { - if valid_flags.iter().position(|&x| x == flagname).is_none() { - error(location, format!("filter(type=\"{}\") called with invalid '{}' flag '{}'", typevalue, flagfieldname, flagname)) + if !valid_flags.iter().any(|&x| x == flagname) { + error(location, format!("filter(type=\"{typevalue}\") called with invalid '{flagfieldname}' flag '{flagname}'")) .with_filter_args(location, typevalue) .register(self.context); } }, Term::Int(0) if can_be_zero => {}, other => { - error(location, format!("filter(type=\"{}\") called with invalid '{}' value '{:?}'", typevalue, flagfieldname, other)) + error(location, format!("filter(type=\"{typevalue}\") called with invalid '{flagfieldname}' value '{other:?}'")) .with_filter_args(location, typevalue) .register(self.context); }, } }, _ => { - error(location, format!("filter(type=\"{}\"), extremely invalid value passed to '{}' field", typevalue, flagfieldname)) + error(location, format!("filter(type=\"{typevalue}\"), extremely invalid value passed to '{flagfieldname}' field")) .with_filter_args(location, typevalue) .register(self.context); - } + }, } } - fn visit_call(&mut self, location: Location, src: TypeRef<'o>, proc: ProcRef<'o>, args: &'o [Expression], is_exact: bool, local_vars: &mut HashMap, RandomState>) -> Analysis<'o> { - self.env.call_tree.entry(self.proc_ref).or_default().push((proc, location, self.inside_newcontext != 0)); + fn visit_call( + &mut self, + location: Location, + src: TypeRef<'o>, + proc: ProcRef<'o>, + args: &'o [Expression], + is_exact: bool, + local_vars: &mut HashMap>, + ) -> Analysis<'o> { + self.env.call_tree.entry(self.proc_ref).or_default().push(( + proc, + location, + self.inside_newcontext != 0, + )); if let Some((privateproc, true, decllocation)) = self.env.private.get_self_or_parent(proc) { if self.ty != privateproc.ty() { - error(location, format!("{} attempting to call private proc {}, types do not match", self.proc_ref, privateproc)) - .with_errortype("private_proc") - .with_note(decllocation, "prohibited by this private_proc annotation") - .register(self.context); + error( + location, + format!( + "{} attempting to call private proc {}, types do not match", + self.proc_ref, privateproc + ), + ) + .with_errortype("private_proc") + .with_note(decllocation, "prohibited by this private_proc annotation") + .register(self.context); } } // identify and register kwargs used let mut any_kwargs_yet = false; - let mut param_name_map = HashMap::with_hasher(RandomState::default()); - let mut param_expr_map = HashMap::with_hasher(RandomState::default()); - let mut param_idx_map = HashMap::with_hasher(RandomState::default()); + let mut param_name_map = HashMap::new(); + let mut param_expr_map = HashMap::new(); + let mut param_idx_map = HashMap::new(); let mut param_idx = 0; let mut arglist_used = false; @@ -2148,10 +3160,13 @@ impl<'o, 's> AnalyzeProc<'o, 's> { let mut argument_value = arg; let mut this_kwarg = None; match arg { - Expression::AssignOp { op: AssignOp::Assign, lhs, rhs } => { + Expression::AssignOp { + op: AssignOp::Assign, + lhs, + rhs, + } => { match lhs.as_term() { - Some(Term::Ident(name)) | - Some(Term::String(name)) => { + Some(Term::Ident(name)) | Some(Term::String(name)) => { // Don't visit_expression the kwarg key. any_kwargs_yet = true; this_kwarg = Some(name); @@ -2160,12 +3175,19 @@ impl<'o, 's> AnalyzeProc<'o, 's> { // Check that that kwarg actually exists. if !proc.parameters.iter().any(|p| p.name == *name) { // Search for a child proc that does have this keyword argument. - let mut error = error(location, - format!("bad keyword argument {:?} to {}", name, proc)); + let mut error = error( + location, + format!("bad keyword argument {name:?} to {proc}"), + ); proc.recurse_children(&mut |child_proc| { - if child_proc.ty() == proc.ty() { return } + if child_proc.ty() == proc.ty() { + return; + } if child_proc.parameters.iter().any(|p| p.name == *name) { - error.add_note(child_proc.location, format!("an override has this parameter: {}", child_proc)); + error.add_note( + child_proc.location, + format!("an override has this parameter: {child_proc}"), + ); } }); error.register(self.context); @@ -2175,38 +3197,52 @@ impl<'o, 's> AnalyzeProc<'o, 's> { // type the proc actually appears on, so that // calling /datum/foo() on a /datum/A won't // complain about /datum/B/foo(). - self.env.used_kwargs.entry(format!("{}/proc/{}", src, proc.name())) + self.env + .used_kwargs + .entry(format!("{}/proc/{}", src, proc.name())) .or_insert_with(|| KwargInfo { location: proc.location, - .. Default::default() + ..Default::default() }) .called_at // TODO: use a more accurate location .entry(name.clone()) .and_modify(|ca| ca.others += 1) .or_insert(CalledAt { - location: location, + location, others: 0, }); } - } - _ => {} + }, + _ => {}, } }, expr => { if let Some(Term::Call(callname, _)) = expr.as_term() { // only interested in the first expression being arglist - if callname.as_str() == "arglist" && param_name_map.len() == 0 && param_idx == 0 { + if callname.as_str() == "arglist" + && param_name_map.is_empty() + && param_idx == 0 + { arglist_used = true; } } }, } - if any_kwargs_yet && this_kwarg.is_none() && !(proc.ty().is_root() && proc.name() == "animate") { + if any_kwargs_yet + && this_kwarg.is_none() + && !(proc.ty().is_root() && proc.name() == "animate") + { // TODO: don't hardcode the animate() exception - error(location, format!("proc called with non-kwargs after kwargs: {}()", proc.name())) - .register(self.context); + error( + location, + format!( + "proc called with non-kwargs after kwargs: {}()", + proc.name() + ), + ) + .register(self.context); } let analysis = self.visit_expression(location, argument_value, None, local_vars); @@ -2219,37 +3255,82 @@ impl<'o, 's> AnalyzeProc<'o, 's> { } } + if proc.ty().is_root() && proc.name() == "astype" { + if let Some(type_val) = param_idx_map.get(&1) { + if let Some(Constant::Prefab(path)) = type_val.clone().value { + let type_val = path + .path + .iter() + .map(|x| "/".to_owned() + x) + .collect::>() + .join(""); + + if let Some(path) = self.objtree.find(&type_val) { + return Analysis::from_static_type(path); + } + } + } + + if let Some(type_val) = param_idx_map.get(&0) { + return Analysis::from(type_val.clone().static_ty); + } + } + // filter call checking // TODO: some filters have limits for their numerical params // eg "rays" type "threshold" param defaults to 0.5, can be 0 to 1 if proc.ty().is_root() && proc.name() == "filter" { - guard!(let Some(typename) = param_name_map.get("type") else { + let Some(typename) = param_name_map.get("type") else { if !arglist_used { - error(location, "filter() called without mandatory keyword parameter 'type'") - .register(self.context); + error( + location, + "filter() called without mandatory keyword parameter 'type'", + ) + .register(self.context); } // regardless, we're done here - return Analysis::empty() - }); - guard!(let Some(Constant::String(typevalue)) = &typename.value else { - error(location, format!("filter() called with non-string type keyword parameter value '{:?}'", typename.value)) - .register(self.context); - return Analysis::empty() - }); - guard!(let Some(arglist) = VALID_FILTER_TYPES.get(&typevalue) else { - error(location, format!("filter() called with invalid type keyword parameter value '{}'", typevalue)) - .register(self.context); - return Analysis::empty() - }); + return Analysis::empty(); + }; + let Some(Constant::String(typevalue)) = &typename.value else { + error( + location, + format!( + "filter() called with non-string type keyword parameter value '{:?}'", + typename.value + ), + ) + .register(self.context); + return Analysis::empty(); + }; + let Some(arglist) = VALID_FILTER_TYPES.get(typevalue) else { + error( + location, + format!( + "filter() called with invalid type keyword parameter value '{typevalue}'" + ), + ) + .register(self.context); + return Analysis::empty(); + }; for arg in param_name_map.keys() { - if *arg != "type" && arglist.iter().position(|&x| x == *arg).is_none() { - error(location, format!("filter(type=\"{}\") called with invalid keyword parameter '{}'", typevalue, arg)) + if *arg != "type" && *arg != "name" && !arglist.contains(arg) { + error(location, format!("filter(type=\"{typevalue}\") called with invalid keyword parameter '{arg}'")) .with_filter_args(location, typevalue) .register(self.context); } } - if let Some((flagfieldname, exclusive, can_be_zero, valid_flags)) = VALID_FILTER_FLAGS.get(&typevalue) { + if let Some((flagfieldname, exclusive, can_be_zero, valid_flags)) = + VALID_FILTER_FLAGS.get(typevalue) + { if let Some(flagsvalue) = param_expr_map.get(flagfieldname) { - self.check_filter_flag(flagsvalue, *can_be_zero, location, typevalue, valid_flags, flagfieldname, *exclusive); + self.check_filter_flag( + flagsvalue, + *can_be_zero, + location, + typevalue, + valid_flags, + flagfieldname, + *exclusive, + ); } } } @@ -2264,32 +3345,43 @@ impl<'o, 's> AnalyzeProc<'o, 's> { }; match return_type.evaluate(location, &ec) { Ok(st) => { - let hint = format!("return type evaluated to {:?}", st); + let hint = format!("return type evaluated to {st:?}"); Analysis::from(st).with_fix_hint(location, hint) }, Err(err) => { err.with_component(dm::Component::DreamChecker) .register(self.context); Analysis::empty() - } + }, } } else { - Analysis::empty() - .with_fix_hint(proc.location, format!("add a return type annotation to {}", proc)) + Analysis::empty().with_fix_hint( + proc.location, + format!("add a return type annotation to {proc}"), + ) } } - fn visit_arguments(&mut self, location: Location, args: &'o [Expression], local_vars: &mut HashMap, RandomState>) { + fn visit_arguments( + &mut self, + location: Location, + args: &'o [Expression], + local_vars: &mut HashMap>, + ) { for arg in args { let mut argument_value = arg; - if let Expression::AssignOp { op: AssignOp::Assign, lhs, rhs } = arg { + if let Expression::AssignOp { + op: AssignOp::Assign, + lhs, + rhs, + } = arg + { match lhs.as_term() { - Some(Term::Ident(_name)) | - Some(Term::String(_name)) => { + Some(Term::Ident(_name)) | Some(Term::String(_name)) => { // Don't visit_expression the kwarg key. argument_value = rhs; - } - _ => {} + }, + _ => {}, } } diff --git a/crates/dreamchecker/src/main.rs b/crates/dreamchecker/src/main.rs index 204383a8..2ba5ec6c 100644 --- a/crates/dreamchecker/src/main.rs +++ b/crates/dreamchecker/src/main.rs @@ -1,8 +1,8 @@ //! DreamChecker, a robust static analysis and typechecking engine for //! DreamMaker. -extern crate dreammaker as dm; extern crate dreamchecker; +extern crate dreammaker as dm; #[macro_use] extern crate serde_json; @@ -17,14 +17,17 @@ fn main() { let mut parse_only = false; let mut args = std::env::args(); - let _ = args.next(); // skip executable name + let _ = args.next(); // skip executable name while let Some(arg) = args.next() { if arg == "-V" || arg == "--version" { println!( - "dreamchecker {} Copyright (C) 2017-2021 Tad Hardesty", + "dreamchecker {} Copyright (C) 2017-2025 Tad Hardesty", env!("CARGO_PKG_VERSION") ); - println!("{}", include_str!(concat!(env!("OUT_DIR"), "/build-info.txt"))); + println!( + "{}", + include_str!(concat!(env!("OUT_DIR"), "/build-info.txt")) + ); println!("This program comes with ABSOLUTELY NO WARRANTY. This is free software,"); println!("and you are welcome to redistribute it under the conditions of the GNU"); println!("General Public License version 3."); @@ -38,29 +41,30 @@ fn main() { } else if arg == "--parse-only" { parse_only = true; } else { - eprintln!("unknown argument: {}", arg); + eprintln!("unknown argument: {arg}"); return; } } let dme = environment .map(std::path::PathBuf::from) - .unwrap_or_else(|| dm::detect_environment_default() - .expect("error detecting .dme") - .expect("no .dme found")); + .unwrap_or_else(|| { + dm::detect_environment_default() + .expect("error detecting .dme") + .expect("no .dme found") + }); let mut context = dm::Context::default(); + context.set_print_severity(Some(dm::Severity::Info)); if let Some(filepath) = config_file { context.force_config(filepath.as_ref()); } else { context.autodetect_config(&dme); } - context.set_print_severity(Some(dm::Severity::Info)); println!("============================================================"); println!("Parsing {}...\n", dme.display()); - let pp = dm::preprocessor::Preprocessor::new(&context, dme) - .expect("i/o error opening .dme"); + let pp = dm::preprocessor::Preprocessor::new(&context, dme).expect("i/o error opening .dme"); let indents = dm::indents::IndentProcessor::new(&context, pp); let mut parser = dm::parser::Parser::new(&context, indents); parser.enable_procs(); @@ -71,8 +75,12 @@ fn main() { } println!("============================================================"); - let errors = context.errors().iter().filter(|each| each.severity() <= dm::Severity::Info).count(); - println!("Found {} diagnostics", errors); + let errors = context + .errors() + .iter() + .filter(|each| each.severity() <= dm::Severity::Info) + .count(); + println!("Found {errors} diagnostics"); if json { serde_json::to_writer(std::io::stdout().lock(), &json! {{ diff --git a/crates/dreamchecker/src/switch_rand_range.rs b/crates/dreamchecker/src/switch_rand_range.rs new file mode 100644 index 00000000..6627e1db --- /dev/null +++ b/crates/dreamchecker/src/switch_rand_range.rs @@ -0,0 +1,112 @@ +use std::borrow::Borrow; + +use dm::ast::*; +use dm::{Context, DMError, Location, Severity}; + +/** + * Checks for mistakes in switches of the form `switch(rand(L, H))`. + * If some cases lie outside of the [L, H] range or the whole [L, H] range is not covered by all the cases a warning is issued. + */ +pub fn check_switch_rand_range( + input: &Expression, + cases: &SwitchCases, + default: &Option, + location: Location, + context: &Context, +) { + let (rand_start, rand_end) = if let Some(range) = get_rand_range(input) { + range + } else { + return; + }; + + let mut case_ranges = Vec::with_capacity(cases.len()); + for case_block in cases.iter() { + let location = case_block.0.location; + for case in case_block.0.elem.iter() { + if let Some((start, end)) = get_case_range(case, location) { + let start = start.ceil() as i32; + let end = end.floor() as i32; + if start <= rand_end && end >= rand_start { + case_ranges.push((start, end)); + } else { + DMError::new(location, format!("Case range '{start} to {end}' will never trigger as it is outside the rand() range {rand_start} to {rand_end}")) + .with_component(dm::Component::DreamChecker) + .set_severity(Severity::Warning) + .register(context); + } + } + } + } + + if default.is_some() { + // covers the whole range directly so no need to check for gaps + return; + } + + case_ranges.sort_by(|a, b| a.0.cmp(&b.0)); + let mut first_uncovered = rand_start; + for (start, end) in case_ranges.iter() { + if *start > first_uncovered { + break; + } else { + first_uncovered = std::cmp::max(first_uncovered, end + 1); + } + } + + if first_uncovered <= rand_end { + DMError::new( + location, + format!( + "Switch branches on rand() with range {rand_start} to {rand_end} but no case branch triggers for {first_uncovered}" + ), + ) + .with_component(dm::Component::DreamChecker) + .set_severity(Severity::Warning) + .register(context); + } +} + +fn get_case_range(case: &Case, location: Location) -> Option<(f32, f32)> { + match case { + Case::Exact(ref value) => { + let value = value + .to_owned() + .simple_evaluate(location) + .ok()? + .to_float()?; + Some((value, value)) + }, + Case::Range(ref min, ref max) => { + let min = min.to_owned().simple_evaluate(location).ok()?.to_float()?; + let max = max.to_owned().simple_evaluate(location).ok()?.to_float()?; + Some((min, max)) + }, + } +} + +fn get_rand_range(maybe_rand: &Expression) -> Option<(i32, i32)> { + let (term, location) = match maybe_rand { + Expression::Base { term, follow } if follow.is_empty() => (&term.elem, term.location), + _ => return None, + }; + + let rand_args: &[Expression] = match term { + Term::Call(proc_name, args) if proc_name.as_str() == "rand" => args.borrow(), + _ => return None, + }; + + let (min, max) = match rand_args { + [min, max] => ( + min.to_owned().simple_evaluate(location).ok()?.to_float()?, + max.to_owned().simple_evaluate(location).ok()?.to_float()?, + ), + [max] => ( + 0., + max.to_owned().simple_evaluate(location).ok()?.to_float()?, + ), + _ => return None, + }; + + Some((min.ceil() as i32, max.floor() as i32)) +} diff --git a/crates/dreamchecker/src/test_helpers.rs b/crates/dreamchecker/src/test_helpers.rs index e0ea5e0c..5102e7f2 100644 --- a/crates/dreamchecker/src/test_helpers.rs +++ b/crates/dreamchecker/src/test_helpers.rs @@ -1,14 +1,18 @@ use dm::Context; use std::borrow::Cow; -use crate::{run_inner}; +use crate::run_inner; pub const NO_ERRORS: &[(u32, u16, &str)] = &[]; pub fn parse_a_file_for_test>>(buffer: S) -> Context { let context = Context::default(); - let pp = dm::preprocessor::Preprocessor::from_buffer(&context, "unit_tests.rs".into(), buffer.into()); + let pp = dm::preprocessor::Preprocessor::from_buffer( + &context, + "unit_tests.rs".into(), + buffer.into(), + ); let indents = dm::indents::IndentProcessor::new(&context, pp); @@ -32,7 +36,7 @@ pub fn check_errors_match>>(buffer: S, errorlist: &[(u || nexterror.description() != *desc { panic!( - "possible feature regression in dreamchecker, expected {}:{}:{}, found {}:{}:{}", + "possible feature regression in dreamchecker:\nexpected error: {}:{}:{}\nfound error: {}:{}:{}", *line, *column, *desc, diff --git a/crates/dreamchecker/src/type_expr.rs b/crates/dreamchecker/src/type_expr.rs index 6e9c1430..e7386a5c 100644 --- a/crates/dreamchecker/src/type_expr.rs +++ b/crates/dreamchecker/src/type_expr.rs @@ -1,21 +1,19 @@ //! Support for "type expressions", used in evaluating dynamic/generic return //! types. -use std::collections::HashMap; +use foldhash::HashMap; use dm::ast::*; use dm::constants::Constant; use dm::objtree::{ObjectTree, ProcRef}; use dm::{DMError, Location}; -use ahash::RandomState; - use crate::{Analysis, StaticType}; pub struct TypeExprContext<'o, 't> { pub objtree: &'o ObjectTree, - pub param_name_map: HashMap<&'t str, Analysis<'o>, RandomState>, - pub param_idx_map: HashMap, RandomState>, + pub param_name_map: HashMap<&'t str, Analysis<'o>>, + pub param_idx_map: HashMap>, } impl<'o, 't> TypeExprContext<'o, 't> { @@ -86,7 +84,7 @@ impl<'o> TypeExpr<'o> { } else { else_.evaluate(location, ec) } - } + }, TypeExpr::ParamTypepath { name, @@ -102,7 +100,7 @@ impl<'o> TypeExpr<'o> { } else { Ok(StaticType::None) } - } + }, TypeExpr::ParamStaticType { name, @@ -114,7 +112,7 @@ impl<'o> TypeExpr<'o> { } else { Ok(StaticType::None) } - } + }, } } } @@ -137,16 +135,13 @@ impl<'o> TypeExprCompiler<'o> { expr: &Expression, ) -> Result, DMError> { match expr { - Expression::Base { - term, - follow, - } => { + Expression::Base { term, follow } => { let mut ty = self.visit_term(term.location, &term.elem)?; for each in follow.iter() { ty = self.visit_follow(each.location, ty, &each.elem)?; } Ok(ty) - } + }, Expression::BinaryOp { op: BinaryOp::Or, lhs, @@ -160,7 +155,7 @@ impl<'o> TypeExprCompiler<'o> { if_: Box::new(lty), else_: Box::new(rty), }) - } + }, Expression::BinaryOp { op: BinaryOp::And, lhs, @@ -174,7 +169,7 @@ impl<'o> TypeExprCompiler<'o> { if_: Box::new(rty), else_: Box::new(lty), }) - } + }, Expression::TernaryOp { cond, if_, else_ } => Ok(TypeExpr::Condition { cond: Box::new(self.visit_expression(location, cond)?), if_: Box::new(self.visit_expression(location, if_)?), @@ -200,9 +195,9 @@ impl<'o> TypeExprCompiler<'o> { } Err(DMError::new( location, - format!("type expr: no such parameter {:?}", unscoped_name), + format!("type expr: no such parameter {unscoped_name:?}"), )) - } + }, Term::Expr(expr) => self.visit_expression(location, expr), @@ -210,7 +205,7 @@ impl<'o> TypeExprCompiler<'o> { let bits: Vec<_> = fab.path.iter().map(|(_, name)| name.to_owned()).collect(); let ty = crate::static_type(self.objtree, location, &bits)?; Ok(TypeExpr::from(ty)) - } + }, _ => Err(DMError::new(location, "type expr: bad term node")), } @@ -263,7 +258,10 @@ impl<'o> TypeExprCompiler<'o> { )), }, - _ => Err(DMError::new(location, format!("type expr: bad follow node {:?}", rhs))), + _ => Err(DMError::new( + location, + format!("type expr: bad follow node {rhs:?}"), + )), } } } diff --git a/crates/dreamchecker/tests/branch_eval_tests.rs b/crates/dreamchecker/tests/branch_eval_tests.rs index 7e085331..098b7b5f 100644 --- a/crates/dreamchecker/tests/branch_eval_tests.rs +++ b/crates/dreamchecker/tests/branch_eval_tests.rs @@ -1,4 +1,3 @@ - extern crate dreamchecker as dc; use dc::test_helpers::*; @@ -15,13 +14,12 @@ fn const_eval() { if(1) return return -"##.trim(); +"## + .trim(); check_errors_match(code, CONST_EVAL_ERRORS); } -pub const IF_ELSE_ERRORS: &[(u32, u16, &str)] = &[ - (6, 5, "possible unreachable code here"), -]; +pub const IF_ELSE_ERRORS: &[(u32, u16, &str)] = &[(6, 5, "possible unreachable code here")]; #[test] fn if_else() { @@ -32,16 +30,25 @@ fn if_else() { else return return -"##.trim(); +"## + .trim(); check_errors_match(code, IF_ELSE_ERRORS); } pub const IF_ARMS_ERRORS: &[(u32, u16, &str)] = &[ (2, 7, "control flow condition is a static term"), (2, 7, "if condition is always true"), - (4, 12, "unreachable if block, preceeding if/elseif condition(s) are always true"), + ( + 4, + 12, + "unreachable if block, preceeding if/elseif condition(s) are always true", + ), // TODO: fix location reporting on this - (7, 9, "unreachable else block, preceeding if/elseif condition(s) are always true"), + ( + 7, + 9, + "unreachable else block, preceeding if/elseif condition(s) are always true", + ), ]; #[test] @@ -54,13 +61,13 @@ fn if_arms() { return else return -"##.trim(); +"## + .trim(); check_errors_match(code, IF_ARMS_ERRORS); } -pub const DO_WHILE_ERRORS: &[(u32, u16, &str)] = &[ - (2, 5, "do while terminates without ever reaching condition"), -]; +pub const DO_WHILE_ERRORS: &[(u32, u16, &str)] = + &[(2, 5, "do while terminates without ever reaching condition")]; #[test] fn do_while() { @@ -69,7 +76,8 @@ fn do_while() { do return while(prob(50)) -"##.trim(); +"## + .trim(); check_errors_match(code, DO_WHILE_ERRORS); } @@ -92,6 +100,74 @@ fn for_loop_condition() { for(var/z = 1; z <= 6; z++) // Legit, should have no error break return -"##.trim(); +"## + .trim(); check_errors_match(code, FOR_LOOP_CONDITION_ERRORS); } + +#[test] +fn for_kv_check() { + let code = r##" +/proc/test() + var/alist/A = alist() + for (var/k, v in A) + world.log << k + world.log << v + +"## + .trim(); + check_errors_match(code, NO_ERRORS); +} + +pub const FOR_KV_VAR_ERROR: &[(u32, u16, &str)] = + &[(3, 19, "for (var/key, value) requires a 'var' keyword")]; + +#[test] +fn for_kv_var_check() { + let code = r##" +/proc/test() + var/alist/A = alist() + for (k, v in A) + world.log << k + world.log << v + +"## + .trim(); + check_errors_match(code, FOR_KV_VAR_ERROR); +} + +pub const FOR_KV_VALUE_ERROR: &[(u32, u16, &str)] = &[( + 3, + 23, + "value must be a variable in a for (var/key, value) statement", +)]; + +#[test] +fn for_kv_value_check() { + let code = r##" +/proc/test() + var/alist/A = alist() + for (var/k, 0 in A) + world.log << k +"## + .trim(); + check_errors_match(code, FOR_KV_VALUE_ERROR); +} + +pub const FOR_KV_KEY_ERROR: &[(u32, u16, &str)] = &[( + 3, + 27, + "cannot assigned a value to var/key in a for(var/key, value) statement", +)]; + +#[test] +fn for_kv_key_check() { + let code = r##" +/proc/test() + var/alist/A = alist() + for (var/k = 5, v in A) + world.log << k +"## + .trim(); + check_errors_match(code, FOR_KV_KEY_ERROR); +} diff --git a/crates/dreamchecker/tests/call_ext_tests.rs b/crates/dreamchecker/tests/call_ext_tests.rs new file mode 100644 index 00000000..bd8a816f --- /dev/null +++ b/crates/dreamchecker/tests/call_ext_tests.rs @@ -0,0 +1,16 @@ +extern crate dreamchecker as dc; + +use dc::test_helpers::*; + +pub const CALL_EXT_MISSING_CALL_ERRORS: &[(u32, u16, &str)] = + &[(2, 19, "got ';', expected one of: '('")]; + +#[test] +fn call_ext_missing_call() { + let code = r##" +/proc/f() + call_ext(1, 2) +"## + .trim(); + check_errors_match(code, CALL_EXT_MISSING_CALL_ERRORS); +} diff --git a/crates/dreamchecker/tests/directive_tests.rs b/crates/dreamchecker/tests/directive_tests.rs index a9570866..09b3fccf 100644 --- a/crates/dreamchecker/tests/directive_tests.rs +++ b/crates/dreamchecker/tests/directive_tests.rs @@ -1,11 +1,9 @@ - extern crate dreamchecker as dc; use dc::test_helpers::*; -pub const TRUE_SUB_ERRORS: &[(u32, u16, &str)] = &[ - (4, 18, "proc never calls parent, required by /mob/proc/test"), -]; +pub const TRUE_SUB_ERRORS: &[(u32, u16, &str)] = + &[(4, 18, "proc never calls parent, required by /mob/proc/test")]; #[test] fn true_substitution() { @@ -15,7 +13,8 @@ fn true_substitution() { /mob/subtype/test() return -"##.trim(); +"## + .trim(); check_errors_match(code, TRUE_SUB_ERRORS); } @@ -29,7 +28,8 @@ fn call_parent() { return /mob/anothertype/test() ..() -"##.trim(); +"## + .trim(); check_errors_match(code, TRUE_SUB_ERRORS); } @@ -42,13 +42,13 @@ fn call_parent_disable() { /mob/subtype/test() set SpacemanDMM_should_call_parent = 0 return -"##.trim(); +"## + .trim(); check_errors_match(code, NO_ERRORS); } -pub const NO_OVERRIDE_ERRORS: &[(u32, u16, &str)] = &[ - (4, 18, "proc overrides parent, prohibited by /mob/proc/test"), -]; +pub const NO_OVERRIDE_ERRORS: &[(u32, u16, &str)] = + &[(4, 18, "proc overrides parent, prohibited by /mob/proc/test")]; #[test] fn no_override() { @@ -58,7 +58,21 @@ fn no_override() { /mob/subtype/test() return -"##.trim(); +"## + .trim(); + check_errors_match(code, NO_OVERRIDE_ERRORS); +} + +#[test] +fn final_proc() { + let code = r##" +/mob/proc/final/test() + return + +/mob/subtype/test() + return +"## + .trim(); check_errors_match(code, NO_OVERRIDE_ERRORS); } @@ -76,7 +90,22 @@ fn no_override_disable() { /mob/subtype/test() set SpacemanDMM_should_not_override = 0 return -"##.trim(); +"## + .trim(); + check_errors_match(code, NO_OVERRIDE_DISABLE_ERRORS); +} + +#[test] +fn final_proc_intermix() { + let code = r##" +/mob/proc/final/test() + return + +/mob/subtype/test() + set SpacemanDMM_should_not_override = 0 + return +"## + .trim(); check_errors_match(code, NO_OVERRIDE_DISABLE_ERRORS); } @@ -89,13 +118,12 @@ fn can_be_redefined() { /mob/test() return -"##.trim(); +"## + .trim(); check_errors_match(code, NO_ERRORS); } -pub const NO_CAN_BE_REDEFINED_ERRORS: &[(u32, u16, &str)] = &[ - (4, 10, "redefining proc /mob/test"), -]; +pub const NO_CAN_BE_REDEFINED_ERRORS: &[(u32, u16, &str)] = &[(4, 10, "redefining proc /mob/test")]; #[test] fn no_can_be_redefined() { @@ -105,6 +133,7 @@ fn no_can_be_redefined() { /mob/test() return -"##.trim(); +"## + .trim(); check_errors_match(code, NO_CAN_BE_REDEFINED_ERRORS); } diff --git a/crates/dreamchecker/tests/kwargs_tests.rs b/crates/dreamchecker/tests/kwargs_tests.rs index 0ba97435..8c5171a0 100644 --- a/crates/dreamchecker/tests/kwargs_tests.rs +++ b/crates/dreamchecker/tests/kwargs_tests.rs @@ -1,11 +1,9 @@ - extern crate dreamchecker as dc; use dc::test_helpers::*; -pub const AFTER_KWARG_ERRORS: &[(u32, u16, &str)] = &[ - (3, 5, "proc called with non-kwargs after kwargs: foo()"), -]; +pub const AFTER_KWARG_ERRORS: &[(u32, u16, &str)] = + &[(3, 5, "proc called with non-kwargs after kwargs: foo()")]; #[test] fn after_kwarg() { @@ -13,23 +11,52 @@ fn after_kwarg() { /proc/foo(arg1, arg2, arg3) /proc/test() foo(arg2=1, 1) -"##.trim(); +"## + .trim(); check_errors_match(code, AFTER_KWARG_ERRORS); } pub const FILTER_KWARGS_ERRORS: &[(u32, u16, &str)] = &[ - (4, 5, "filter(type=\"color\") called with invalid 'space' value 'Null'"), - (15, 5, "filter(type=\"alpha\") called with invalid keyword parameter 'color'"), - (16, 5, "filter(type=\"blur\") called with invalid keyword parameter 'x'"), - (17, 5, "filter() called with invalid type keyword parameter value 'fakename'"), - (18, 5, "filter() called without mandatory keyword parameter 'type'"), - (19, 5, "filter() called without mandatory keyword parameter 'type'"), - (20, 5, "filter(type=\"wave\") called with invalid keyword parameter 'color'"), + ( + 4, + 5, + "filter(type=\"color\") called with invalid 'space' value 'Null'", + ), + ( + 15, + 5, + "filter(type=\"alpha\") called with invalid keyword parameter 'color'", + ), + ( + 16, + 5, + "filter(type=\"blur\") called with invalid keyword parameter 'x'", + ), + ( + 17, + 5, + "filter() called with invalid type keyword parameter value 'fakename'", + ), + ( + 18, + 5, + "filter() called without mandatory keyword parameter 'type'", + ), + ( + 19, + 5, + "filter() called without mandatory keyword parameter 'type'", + ), + ( + 20, + 5, + "filter(type=\"wave\") called with invalid keyword parameter 'color'", + ), ]; #[test] fn filter_kwarg() { - let code = r##" + let code = r#" /proc/test() filter(type="alpha", x=1, y=2, icon=null, render_source=null, flags=0) filter(type="angular_blur", x=1, y=2, size=null) @@ -50,6 +77,6 @@ fn filter_kwarg() { filter(x=4) filter("alpha", x=1, flags=MASK_INVERSE|MASK_INVERSE|MASK_INVERSE|MASK_INVERSE|MASK_INVERSE|MASK_INVERSE) filter(type="wave", color=null) -"##.trim(); +"#.trim(); check_errors_match(code, FILTER_KWARGS_ERRORS); } diff --git a/crates/dreamchecker/tests/local_vars_tests.rs b/crates/dreamchecker/tests/local_vars_tests.rs index ffe538e5..a04e1886 100644 --- a/crates/dreamchecker/tests/local_vars_tests.rs +++ b/crates/dreamchecker/tests/local_vars_tests.rs @@ -1,4 +1,3 @@ - extern crate dreamchecker as dc; use dc::test_helpers::check_errors_match; @@ -25,6 +24,7 @@ fn local_scope() { alabel: var/bar bar++ -"##.trim(); +"## + .trim(); check_errors_match(code, LOCAL_SCOPE_ERRORS); } diff --git a/crates/dreamchecker/tests/new_tests.rs b/crates/dreamchecker/tests/new_tests.rs index 2f434912..18c6aad9 100644 --- a/crates/dreamchecker/tests/new_tests.rs +++ b/crates/dreamchecker/tests/new_tests.rs @@ -1,15 +1,16 @@ - extern crate dreamchecker as dc; use dc::test_helpers::*; -pub const NEW_DOT_ERRORS: &[(u32, u16, &str)] = &[ - (12, 14, "got '(', expected one of: operator, field access, ';'"), -]; +pub const NEW_DOT_ERRORS: &[(u32, u16, &str)] = &[( + 12, + 14, + "got '(', expected one of: operator, field access, ';'", +)]; #[test] fn new_dot() { - let code = r##" + let code = r#" /mob/subtype /mob/proc/foo() /mob/proc/test() @@ -24,13 +25,16 @@ fn new_dot() { new foo()() new /obj[0]() // TODO: see parser.rs new 2 + 2() // TODO: see parser.rs -"##.trim(); +"# + .trim(); check_errors_match(code, NEW_DOT_ERRORS); } -pub const NEW_PRECEDENCE_ERRORS: &[(u32, u16, &str)] = &[ - (4, 13, "got '(', expected one of: operator, field access, ';'"), -]; +pub const NEW_PRECEDENCE_ERRORS: &[(u32, u16, &str)] = &[( + 4, + 13, + "got '(', expected one of: operator, field access, ';'", +)]; #[test] fn new_precedence() { @@ -39,6 +43,7 @@ fn new_precedence() { /mob/proc/foo() /mob/proc/test() new L[1]() -"##.trim(); +"## + .trim(); check_errors_match(code, NEW_PRECEDENCE_ERRORS); } diff --git a/crates/dreamchecker/tests/operator_tests.rs b/crates/dreamchecker/tests/operator_tests.rs index 94907d6f..2dd1efa9 100644 --- a/crates/dreamchecker/tests/operator_tests.rs +++ b/crates/dreamchecker/tests/operator_tests.rs @@ -30,13 +30,16 @@ fn in_ambig() { return if((i ? 1 : 2) in list()) return -"##.trim(); +"## + .trim(); check_errors_match(code, IN_AMBIG_ERRORS); } -pub const TERNARY_IN_AMBIG_ERRORS: &[(u32, u16, &str)] = &[ - (2, 14, "got \'in\', expected one of: operator, field access, \':\'"), -]; +pub const TERNARY_IN_AMBIG_ERRORS: &[(u32, u16, &str)] = &[( + 2, + 14, + "got \'in\', expected one of: operator, field access, \':\'", +)]; #[test] fn ambig_in_ternary_cond() { @@ -44,13 +47,16 @@ fn ambig_in_ternary_cond() { /proc/test() if(i ? 1 in list() : 2) return -"##.trim(); +"## + .trim(); check_errors_match(code, TERNARY_IN_AMBIG_ERRORS); } -pub const OP_OVERLOAD_ERRORS: &[(u32, u16, &str)] = &[ - (6, 6, "Attempting operator++ on a /mob which does not overload operator++"), -]; +pub const OP_OVERLOAD_ERRORS: &[(u32, u16, &str)] = &[( + 6, + 6, + "Attempting operator++ on a /mob which does not overload operator++", +)]; #[test] fn operator_overload() { @@ -63,7 +69,8 @@ fn operator_overload() { M++ var/mob/test/T = new T++ -"##.trim(); +"## + .trim(); check_errors_match(code, OP_OVERLOAD_ERRORS); } @@ -87,6 +94,7 @@ fn ambigous_not_bitwise() { return if (1++ & 1) return -"##.trim(); +"## + .trim(); check_errors_match(code, NOT_AMBIG_BITWISE_ERRORS); } diff --git a/crates/dreamchecker/tests/private_tests.rs b/crates/dreamchecker/tests/private_tests.rs index 8421fea3..a12a6ee6 100644 --- a/crates/dreamchecker/tests/private_tests.rs +++ b/crates/dreamchecker/tests/private_tests.rs @@ -1,4 +1,3 @@ - extern crate dreamchecker as dc; use dc::test_helpers::*; @@ -31,14 +30,19 @@ fn private_proc() { M.private2() var/mob/subtype/S = new S.private() -"##.trim(); +"## + .trim(); check_errors_match(code, PRIVATE_PROC_ERRORS); } pub const PRIVATE_VAR_ERRORS: &[(u32, u16, &str)] = &[ (5, 9, "/mob/subtype overrides private var \"foo\""), (12, 6, "field \"bar\" on /mob is declared as private"), - (14, 6, "field \"foo\" on /mob/subtype is declared as private"), + ( + 14, + 6, + "field \"foo\" on /mob/subtype is declared as private", + ), ]; #[test] @@ -58,13 +62,22 @@ fn private_var() { M.bar = TRUE var/mob/subtype/S = new S.foo = TRUE -"##.trim(); +"## + .trim(); check_errors_match(code, PRIVATE_VAR_ERRORS); } pub const PROTECTED_PROC_ERRORS: &[(u32, u16, &str)] = &[ - (15, 6, "/obj/proc/test attempting to call protected proc /mob/proc/protected2"), - (17, 6, "/obj/proc/test attempting to call protected proc /mob/proc/protected"), + ( + 15, + 6, + "/obj/proc/test attempting to call protected proc /mob/proc/protected2", + ), + ( + 17, + 6, + "/obj/proc/test attempting to call protected proc /mob/proc/protected", + ), ]; #[test] @@ -87,13 +100,18 @@ fn protected_proc() { M.protected2() var/mob/subtype/S = new S.protected() -"##.trim(); +"## + .trim(); check_errors_match(code, PROTECTED_PROC_ERRORS); } pub const PROTECTED_VAR_ERRORS: &[(u32, u16, &str)] = &[ (12, 6, "field \"bar\" on /mob is declared as protected"), - (14, 6, "field \"foo\" on /mob/subtype is declared as protected"), + ( + 14, + 6, + "field \"foo\" on /mob/subtype is declared as protected", + ), ]; #[test] @@ -113,6 +131,7 @@ fn protected_var() { M.bar = TRUE var/mob/subtype/S = new S.foo = TRUE -"##.trim(); +"## + .trim(); check_errors_match(code, PROTECTED_VAR_ERRORS); } diff --git a/crates/dreamchecker/tests/proc_tests.rs b/crates/dreamchecker/tests/proc_tests.rs index ec6545e9..f5c4d7f0 100644 --- a/crates/dreamchecker/tests/proc_tests.rs +++ b/crates/dreamchecker/tests/proc_tests.rs @@ -1,11 +1,9 @@ - extern crate dreamchecker as dc; use dc::test_helpers::check_errors_match; +use dc::test_helpers::parse_a_file_for_test; -pub const NO_PARENT_ERRORS: &[(u32, u16, &str)] = &[ - (2, 5, "proc has no parent: /mob/proc/test"), -]; +pub const NO_PARENT_ERRORS: &[(u32, u16, &str)] = &[(2, 5, "proc has no parent: /mob/proc/test")]; #[test] fn no_parent() { @@ -13,6 +11,49 @@ fn no_parent() { /mob/proc/test() ..() return -"##.trim(); +"## + .trim(); check_errors_match(code, NO_PARENT_ERRORS); } + +#[test] +fn return_type() { + let code = r##" +/mob/proc/test() as /datum + return + +/mob/proc/test2() as num + return +"## + .trim(); + let context = parse_a_file_for_test(code); + let error_text: Vec = context + .errors() + .iter() + .map(|error| format!("{error}")) + .collect(); + if !error_text.is_empty() { + panic!("\n{}", error_text.join("\n")) + } +} + +pub const RETURN_TYPE_FAILURE_ERRORS: &[(u32, u16, &str)] = &[ + (4, 13, "cannot specify a return type for a proc override"), + (7, 22, "bad input type: 'incorrect'"), +]; + +#[test] +fn return_type_failure() { + let code = r##" +/datum/proc/test() as /datum + return + +/mob/test() as /mob + return + +/mob/proc/test2() as incorrect + return +"## + .trim(); + check_errors_match(code, RETURN_TYPE_FAILURE_ERRORS); +} diff --git a/crates/dreamchecker/tests/sleep_pure_tests.rs b/crates/dreamchecker/tests/sleep_pure_tests.rs index 9a023b6b..722dba1d 100644 --- a/crates/dreamchecker/tests/sleep_pure_tests.rs +++ b/crates/dreamchecker/tests/sleep_pure_tests.rs @@ -1,11 +1,12 @@ - extern crate dreamchecker as dc; use dc::test_helpers::check_errors_match; -pub const SLEEP_ERRORS: &[(u32, u16, &str)] = &[ - (16, 16, "/mob/proc/test3 sets SpacemanDMM_should_not_sleep but calls blocking proc /proc/sleepingproc"), -]; +pub const SLEEP_ERRORS: &[(u32, u16, &str)] = &[( + 16, + 16, + "/mob/proc/test3 sets SpacemanDMM_should_not_sleep but calls blocking proc /proc/sleepingproc", +)]; #[test] fn sleep() { @@ -41,7 +42,8 @@ fn sleep() { /mob/proc/test6() set SpacemanDMM_should_not_sleep = TRUE spawnthensleepproc() -"##.trim(); +"## + .trim(); check_errors_match(code, SLEEP_ERRORS); } @@ -75,7 +77,8 @@ fn sleep2() { sleep(1) /mob/living/thing() . = ..() -"##.trim(); +"## + .trim(); check_errors_match(code, SLEEP_ERRORS2); } @@ -106,7 +109,8 @@ fn sleep3() { sleep(1) /atom/movable/thing() . = ..() -"##.trim(); +"## + .trim(); check_errors_match(code, &[ (8, 23, "/atom/movable/proc/bar calls /atom/movable/proc/foo which has override child proc that sleeps /mob/proc/foo"), ]); @@ -145,17 +149,45 @@ fn sleep4() { /mob/proc/test2() var/client/C = new /client C.MeasureText() -"##.trim(); +"## + .trim(); check_errors_match(code, SLEEP_ERROR4); } +// Test overrides and for regression of issue #267 +pub const SLEEP_ERROR5: &[(u32, u16, &str)] = &[ + (7, 19, "/datum/sub/proc/checker sets SpacemanDMM_should_not_sleep but calls blocking proc /proc/sleeper"), +]; + +#[test] +fn sleep5() { + let code = r##" +/datum/proc/checker() + set SpacemanDMM_should_not_sleep = 1 + +/datum/proc/proxy() + sleeper() + +/datum/sub/checker() + proxy() + +/proc/sleeper() + sleep(1) + +/datum/hijack/proxy() + sleep(1) +"## + .trim(); + check_errors_match(code, SLEEP_ERROR5); +} + pub const PURE_ERRORS: &[(u32, u16, &str)] = &[ (12, 16, "/mob/proc/test2 sets SpacemanDMM_should_be_pure but calls a /proc/impure that does impure operations"), ]; #[test] fn pure() { - let code = r##" + let code = r#" /proc/pure() return 1 /proc/impure() @@ -170,15 +202,15 @@ fn pure() { /mob/proc/test2() set SpacemanDMM_should_be_pure = TRUE bar() -"##.trim(); +"# + .trim(); check_errors_match(code, PURE_ERRORS); } // these tests are separate because the ordering the errors are reported in isn't determinate and I CBF figuring out why -spookydonut Jan 2020 // TODO: find out why -pub const PURE2_ERRORS: &[(u32, u16, &str)] = &[ - (5, 5, "call to pure proc test discards return value"), -]; +pub const PURE2_ERRORS: &[(u32, u16, &str)] = + &[(5, 5, "call to pure proc test discards return value")]; #[test] fn pure2() { @@ -190,6 +222,7 @@ fn pure2() { test() /mob/proc/test3() return test() -"##.trim(); +"## + .trim(); check_errors_match(code, PURE2_ERRORS); } diff --git a/crates/dreamchecker/tests/static_type_tests.rs b/crates/dreamchecker/tests/static_type_tests.rs index cd42b0c6..d874c89d 100644 --- a/crates/dreamchecker/tests/static_type_tests.rs +++ b/crates/dreamchecker/tests/static_type_tests.rs @@ -1,4 +1,3 @@ - extern crate dreamchecker as dc; use dc::test_helpers::*; @@ -17,7 +16,8 @@ fn field_access() { L?[1].name var/atom/movable/particle_holder = new particle_holder.particles.height -"##.trim(); +"## + .trim(); check_errors_match(code, FIELD_ACCESS_ERRORS); } @@ -34,13 +34,12 @@ fn proc_call() { L[1].foo() L?[1].foo() /mob/proc/foo() -"##.trim(); +"## + .trim(); check_errors_match(code, PROC_CALL_ERRORS); } -pub const RETURN_TYPE_ERRORS: &[(u32, u16, &str)] = &[ - (3, 16, "undefined proc: \"foo\" on /atom"), -]; +pub const RETURN_TYPE_ERRORS: &[(u32, u16, &str)] = &[(3, 16, "undefined proc: \"foo\" on /atom")]; #[test] fn return_type() { @@ -49,6 +48,7 @@ fn return_type() { viewers()[1].foo() orange()[1].foo() /mob/proc/foo() -"##.trim(); +"## + .trim(); check_errors_match(code, RETURN_TYPE_ERRORS); } diff --git a/crates/dreamchecker/tests/switch_rand_range_tests.rs b/crates/dreamchecker/tests/switch_rand_range_tests.rs new file mode 100644 index 00000000..d38b5d20 --- /dev/null +++ b/crates/dreamchecker/tests/switch_rand_range_tests.rs @@ -0,0 +1,118 @@ +extern crate dreamchecker as dc; + +use dc::test_helpers::*; + +pub const SWITCH_RAND_INCOMPLETE_ERRORS: &[(u32, u16, &str)] = &[ + ( + 4, + 19, + "Case range '0 to 0' will never trigger as it is outside the rand() range 1 to 3", + ), + ( + 2, + 5, + "Switch branches on rand() with range 1 to 3 but no case branch triggers for 3", + ), +]; + +#[test] +fn switch_rand_incomplete() { + let code = r##" +/proc/test() + switch(rand(1, 3)) + if(0) + return + if(1) + return + if(2) + return +"## + .trim(); + check_errors_match(code, SWITCH_RAND_INCOMPLETE_ERRORS); +} + +pub const SWITCH_RAND_WITH_EVALUATION_ERRORS: &[(u32, u16, &str)] = &[( + 2, + 5, + "Switch branches on rand() with range 2 to 3 but no case branch triggers for 3", +)]; + +#[test] +fn switch_rand_with_evaluation() { + let code = r##" +/proc/test() + switch(rand(1 + 1, 4 - 1)) + if(3 - 1) + return +"## + .trim(); + check_errors_match(code, SWITCH_RAND_WITH_EVALUATION_ERRORS); +} + +#[test] +fn switch_rand_case_ranges() { + let code = r##" +/proc/test() + switch(rand(1, 4)) + if(1 to 2) + return + if(3, 4) + return +"## + .trim(); + check_errors_match(code, &[]); +} + +pub const SWITCH_RAND_DEFAULT_ERRORS: &[(u32, u16, &str)] = &[( + 4, + 19, + "Case range '5 to 5' will never trigger as it is outside the rand() range 1 to 4", +)]; + +#[test] +fn switch_rand_default() { + let code = r##" +/proc/test() + switch(rand(1, 4)) + if(5) + return + if(2) + return + else + return +"## + .trim(); + check_errors_match(code, SWITCH_RAND_DEFAULT_ERRORS); +} + +#[test] +fn switch_rand_floats() { + let code = r##" +/proc/test() + switch(rand(1, 4)) + if(0.5 to 1.5) + return + if(2) + return + if(2.5 to 400) + return +"## + .trim(); + check_errors_match(code, &[]); +} + +#[test] +fn switch_rand_out_of_order() { + let code = r##" +/proc/test() + switch(rand(1, 4)) + if(3 to 4) + return + if(2) + return + if(1) + return +"## + .trim(); + check_errors_match(code, &[]); +} diff --git a/crates/dreamchecker/tests/var_declare_tests.rs b/crates/dreamchecker/tests/var_declare_tests.rs index 0ec546dc..67fbfca7 100644 --- a/crates/dreamchecker/tests/var_declare_tests.rs +++ b/crates/dreamchecker/tests/var_declare_tests.rs @@ -1,11 +1,8 @@ - extern crate dreamchecker as dc; use dc::test_helpers::*; -pub const VAR_DEC_ERRORS: &[(u32, u16, &str)] = &[ - (5, 12, "/mob/subtype redeclares var \"foo\""), -]; +pub const VAR_DEC_ERRORS: &[(u32, u16, &str)] = &[(5, 12, "/mob/subtype redeclares var \"foo\"")]; #[test] fn var_redec() { @@ -15,29 +12,41 @@ fn var_redec() { /mob/subtype var/foo -"##.trim(); +"## + .trim(); check_errors_match(code, VAR_DEC_ERRORS); } -pub const VAR_FINAL_ERRORS: &[(u32, u16, &str)] = &[ - (5, 9, "/mob/subtype overrides final var \"foo\""), -]; +pub const VAR_FINAL_ERRORS: &[(u32, u16, &str)] = + &[(5, 9, "/mob/subtype overrides final var \"foo\"")]; #[test] -fn var_final() { +fn var_spaceman_final() { let code = r##" /mob var/SpacemanDMM_final/foo = 0 /mob/subtype foo = 1 -"##.trim(); +"## + .trim(); check_errors_match(code, VAR_FINAL_ERRORS); } -pub const VAR_UNDECL_ERRORS: &[(u32, u16, &str)] = &[ - (6, 5, "undefined var: \"bar\""), -]; +#[test] +fn var_final() { + let code = r##" +/mob + var/final/foo = 0 + +/mob/subtype + foo = 1 +"## + .trim(); + check_errors_match(code, VAR_FINAL_ERRORS); +} + +pub const VAR_UNDECL_ERRORS: &[(u32, u16, &str)] = &[(6, 5, "undefined var: \"bar\"")]; #[test] fn var_undecl() { @@ -48,7 +57,7 @@ fn var_undecl() { /mob/proc/test() foo++ bar++ -"##.trim(); +"## + .trim(); check_errors_match(code, VAR_UNDECL_ERRORS); } - diff --git a/crates/dreammaker/Cargo.toml b/crates/dreammaker/Cargo.toml index 822a38bc..affcb446 100644 --- a/crates/dreammaker/Cargo.toml +++ b/crates/dreammaker/Cargo.toml @@ -2,23 +2,25 @@ name = "dreammaker" version = "0.1.0" authors = ["Tad Hardesty "] -edition = "2018" +edition = "2021" [dependencies] interval-tree = { path = "../interval-tree" } builtins-proc-macro = { path = "../builtins-proc-macro" } -lodepng = "3.1.0" -bitflags = "1.0.3" -termcolor = "1.0.4" -ordered-float = "2.0.0" -serde = { version = "1.0.103", features = ["derive"] } -serde_derive = "1.0.103" -toml = "0.5.5" -guard = "0.5.0" -phf = { version = "0.10.0", features = ["macros"] } -color_space = "0.5.3" -ahash = "0.7.6" -indexmap = "1.7.0" +lodepng = "3.10.7" +bitflags = "1.3.2" +termcolor = "1.4.1" +ordered-float = "3.9.2" +serde = { version = "1.0.213", features = ["derive"] } +serde_derive = "1.0.213" +toml = "0.5.11" +phf = { version = "0.11.2", features = ["macros"] } +color_space = "0.5.4" +foldhash = "0.2.0" +indexmap = "2.6.0" +derivative = "2.2.0" +get-size = "0.1.4" +get-size-derive = "0.1.3" [dev-dependencies] -walkdir = "2.0.1" +walkdir = "2.5.0" diff --git a/crates/dreammaker/README.md b/crates/dreammaker/README.md index 93ddb33b..b11fae37 100644 --- a/crates/dreammaker/README.md +++ b/crates/dreammaker/README.md @@ -15,4 +15,4 @@ core component of SpacemanDMM and powers the rest of the tooling. * Non-constant initial values for object variables. * Integer constants which are outside of range. -[2072419]: https://secure.byond.com/forum/?post=2072419 \ No newline at end of file +[2072419]: https://www.byond.com/forum/?post=2072419 diff --git a/crates/dreammaker/examples/count-vars.rs b/crates/dreammaker/examples/count-vars.rs index 82d78cab..81a486ea 100644 --- a/crates/dreammaker/examples/count-vars.rs +++ b/crates/dreammaker/examples/count-vars.rs @@ -7,8 +7,7 @@ fn main() { let env = dm::detect_environment_default() .expect("error detecting .dme") .expect("no .dme found"); - let pp = dm::preprocessor::Preprocessor::new(&context, env) - .expect("i/o error opening .dme"); + let pp = dm::preprocessor::Preprocessor::new(&context, env).expect("i/o error opening .dme"); let indents = dm::indents::IndentProcessor::new(&context, pp); let mut parser = dm::parser::Parser::new(&context, indents); parser.enable_procs(); @@ -25,5 +24,10 @@ fn main() { } } }); - println!("decls: {}\noverrides: {}\ntotal: {}", decls, overrides, decls + overrides); + println!( + "decls: {}\noverrides: {}\ntotal: {}", + decls, + overrides, + decls + overrides + ); } diff --git a/crates/dreammaker/examples/flags.rs b/crates/dreammaker/examples/flags.rs index d32c6d1c..a8dc283c 100644 --- a/crates/dreammaker/examples/flags.rs +++ b/crates/dreammaker/examples/flags.rs @@ -32,31 +32,24 @@ fn main() { .unwrap_or(0); // Migrate old flags_1 values to their item_flags equivalents - let lhs = - if flags_1 & (1<<1) != 0 { 1<<8 } else { 0 } | - if flags_1 & (1<<2) != 0 { 1<<7 } else { 0 } | - if flags_1 & (1<<6) != 0 { 1<<9 } else { 0 } | - if flags_1 & (1<<10) != 0 { 1<<6 } else { 0 }; - flags_1 &= !((1<<1) | (1<<2) | (1<<6) | (1<<10)); + let lhs = if flags_1 & (1 << 1) != 0 { 1 << 8 } else { 0 } + | if flags_1 & (1 << 2) != 0 { 1 << 7 } else { 0 } + | if flags_1 & (1 << 6) != 0 { 1 << 9 } else { 0 } + | if flags_1 & (1 << 10) != 0 { 1 << 6 } else { 0 }; + flags_1 &= !((1 << 1) | (1 << 2) | (1 << 6) | (1 << 10)); - let rhs = item_flags & ((1<<6) | (1<<7) | (1<<8) | (1<<9)); + let rhs = item_flags & ((1 << 6) | (1 << 7) | (1 << 8) | (1 << 9)); item_flags &= !rhs; let crossover = if rhs != 0 && lhs != 0 { - panic!( - "flags_1={}, item_flags={}, lhs={}, rhs={}", - flags_1, item_flags, lhs, rhs - ); + panic!("flags_1={flags_1}, item_flags={item_flags}, lhs={lhs}, rhs={rhs}"); } else if lhs != 0 { lhs } else { rhs }; - println!( - "flags_1={}, item_flags={}, crossover={}", - flags_1, item_flags, crossover - ); + println!("flags_1={flags_1}, item_flags={item_flags}, crossover={crossover}"); }); println!("---- anchored example ----"); @@ -70,17 +63,22 @@ fn main() { println!("{} -> {}", ty.path, anch); // print location info for any type with a redundant `anchored = TRUE` - if anch && ty - .parent_type() - .unwrap() - .get_value("anchored") - .unwrap() - .constant - .as_ref() - .unwrap() - .to_bool() + if anch + && ty + .parent_type() + .unwrap() + .get_value("anchored") + .unwrap() + .constant + .as_ref() + .unwrap() + .to_bool() { - println!("{}:{}", ctx.file_path(var.location.file).display(), var.location.line); + println!( + "{}:{}", + ctx.file_path(var.location.file).display(), + var.location.line + ); } }); } diff --git a/crates/dreammaker/examples/lint.rs b/crates/dreammaker/examples/lint.rs index 74ea3fc1..3dc2be53 100644 --- a/crates/dreammaker/examples/lint.rs +++ b/crates/dreammaker/examples/lint.rs @@ -9,8 +9,7 @@ fn main() { let env = dm::detect_environment_default() .expect("error detecting .dme") .expect("no .dme found"); - let pp = dm::preprocessor::Preprocessor::new(&context, env) - .expect("i/o error opening .dme"); + let pp = dm::preprocessor::Preprocessor::new(&context, env).expect("i/o error opening .dme"); let indents = dm::indents::IndentProcessor::new(&context, pp); let mut parser = dm::parser::Parser::new(&context, indents); parser.enable_procs(); diff --git a/crates/dreammaker/src/annotation.rs b/crates/dreammaker/src/annotation.rs index df19cc2c..a43c3b67 100644 --- a/crates/dreammaker/src/annotation.rs +++ b/crates/dreammaker/src/annotation.rs @@ -1,9 +1,13 @@ //! Data structures for the parser to output mappings from input ranges to AST //! elements at those positions. -use interval_tree::{IntervalTree, RangePairIter, RangeInclusive, range}; -use super::Location; +use std::rc::Rc; + +use crate::docs::DocCollection; +use interval_tree::{range, IntervalTree, RangeInclusive, RangePairIter}; + use super::ast::*; +use super::Location; pub type Iter<'a> = RangePairIter<'a, Location, Annotation>; @@ -23,24 +27,28 @@ pub enum Annotation { UnscopedVar(Ident), ScopedCall(Vec, Ident), ScopedVar(Vec, Ident), - ParentCall, // .. - ReturnVal, // . - InSequence(usize), // where in TreePath or TypePath is this ident + ParentCall, // .. + ReturnVal, // . + InSequence(usize), // where in TreePath or TypePath is this ident // a macro is called here, which is defined at this location MacroDefinition(Ident), - MacroUse(String, Location), + MacroUse { + name: String, + definition_location: Location, + docs: Option>, + }, Include(std::path::PathBuf), Resource(std::path::PathBuf), // error annotations, mostly for autocompletion - ScopedMissingIdent(Vec), // when a . is followed by a non-ident + ScopedMissingIdent(Vec), // when a . is followed by a non-ident IncompleteTypePath(TypePath, PathOp), IncompleteTreePath(bool, Vec), - ProcArguments(Vec, String, usize), // Vec empty for unscoped call - ProcArgument(usize), // where in the prog arguments we are + ProcArguments(Vec, String, usize), // Vec empty for unscoped call + ProcArgument(usize), // where in the prog arguments we are } #[derive(Debug)] @@ -60,7 +68,8 @@ impl Default for AnnotationTree { impl AnnotationTree { pub fn insert(&mut self, place: std::ops::Range, value: Annotation) { - self.tree.insert(range(place.start, place.end.pred()), value); + self.tree + .insert(range(place.start, place.end.pred()), value); self.len += 1; } @@ -77,19 +86,19 @@ impl AnnotationTree { self.len == 0 } - pub fn iter(&self) -> Iter { + pub fn iter(&self) -> Iter<'_> { self.tree.iter() } - pub fn get_location(&self, loc: Location) -> Iter { + pub fn get_location(&self, loc: Location) -> Iter<'_> { self.tree.range(range(loc.pred(), loc)) } - pub fn get_range(&self, place: std::ops::Range) -> Iter { + pub fn get_range(&self, place: std::ops::Range) -> Iter<'_> { self.tree.range(range(place.start, place.end.pred())) } - pub fn get_range_raw(&self, place: RangeInclusive) -> Iter { + pub fn get_range_raw(&self, place: RangeInclusive) -> Iter<'_> { self.tree.range(place) } } diff --git a/crates/dreammaker/src/ast.rs b/crates/dreammaker/src/ast.rs index 91de444a..ad9921fe 100644 --- a/crates/dreammaker/src/ast.rs +++ b/crates/dreammaker/src/ast.rs @@ -3,15 +3,24 @@ //! Most AST types can be pretty-printed using the `Display` trait. use std::fmt; use std::iter::FromIterator; + +use get_size::GetSize; +use get_size_derive::GetSize; use phf::phf_map; use crate::error::Location; +/// Arguments for [`Term::Pick`] +pub type PickArgs = [(Option, Expression)]; + +/// Cases for [`Statement::Switch`] +pub type SwitchCases = [(Spanned>, Block)]; + // ---------------------------------------------------------------------------- // Simple enums /// The unary operators, both prefix and postfix. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, GetSize)] pub enum UnaryOp { Neg, Not, @@ -20,12 +29,14 @@ pub enum UnaryOp { PostIncr, PreDecr, PostDecr, + Reference, + Dereference, } impl UnaryOp { /// Prepare to display this unary operator around (to the left or right of) /// its operand. - pub fn around<'a, T: fmt::Display + ?Sized>(self, expr: &'a T) -> impl fmt::Display + 'a { + pub fn around(self, expr: &'_ T) -> impl fmt::Display + '_ { /// A formatting wrapper created by `UnaryOp::around`. struct Around<'a, T: 'a + ?Sized> { op: UnaryOp, @@ -43,6 +54,8 @@ impl UnaryOp { PostIncr => write!(f, "{}++", self.expr), PreDecr => write!(f, "--{}", self.expr), PostDecr => write!(f, "{}--", self.expr), + Reference => write!(f, "&{}", self.expr), + Dereference => write!(f, "*{}", self.expr), } } } @@ -59,6 +72,8 @@ impl UnaryOp { BitNot => "~", PreIncr | PostIncr => "++", PreDecr | PostDecr => "--", + Reference => "&", + Dereference => "*", } } } @@ -66,7 +81,7 @@ impl UnaryOp { /// The DM path operators. /// /// Which path operator is used typically only matters at the start of a path. -#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, GetSize)] pub enum PathOp { /// `/` for absolute pathing. Slash, @@ -93,7 +108,7 @@ impl fmt::Display for PathOp { } /// The binary operators. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, GetSize)] pub enum BinaryOp { Add, Sub, @@ -101,12 +116,14 @@ pub enum BinaryOp { Div, Pow, Mod, + FloatMod, Eq, NotEq, Less, Greater, LessEq, GreaterEq, + LessOrGreater, Equiv, NotEquiv, BitAnd, @@ -117,7 +134,7 @@ pub enum BinaryOp { And, Or, In, - To, // only appears in RHS of `In` + To, // only appears in RHS of `In` } impl fmt::Display for BinaryOp { @@ -130,11 +147,13 @@ impl fmt::Display for BinaryOp { Div => "/", Pow => "**", Mod => "%", + FloatMod => "%%", Eq => "==", NotEq => "!=", Less => "<", Greater => ">", LessEq => "<=", + LessOrGreater => "<=>", GreaterEq => ">=", Equiv => "~=", NotEquiv => "~!", @@ -152,7 +171,7 @@ impl fmt::Display for BinaryOp { } /// The assignment operators, including augmented assignment. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, GetSize)] pub enum AssignOp { Assign, AddAssign, @@ -160,6 +179,7 @@ pub enum AssignOp { MulAssign, DivAssign, ModAssign, + FloatModAssign, AssignInto, BitAndAssign, AndAssign, @@ -180,6 +200,7 @@ impl fmt::Display for AssignOp { MulAssign => "*=", DivAssign => "/=", ModAssign => "%=", + FloatModAssign => "%%=", AssignInto => ":=", BitAndAssign => "&=", AndAssign => "&&=", @@ -221,6 +242,7 @@ augmented! { Mul = MulAssign; Div = DivAssign; Mod = ModAssign; + FloatMod = FloatModAssign; BitAnd = BitAndAssign; BitOr = BitOrAssign; BitXor = BitXorAssign; @@ -237,7 +259,7 @@ pub enum TernaryOp { } /// The possible kinds of access operators for lists -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, GetSize)] pub enum ListAccessKind { /// `[]` Normal, @@ -246,7 +268,7 @@ pub enum ListAccessKind { } /// The possible kinds of index operators, for both fields and methods. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, GetSize)] pub enum PropertyAccessKind { /// `a.b` Dot, @@ -256,6 +278,8 @@ pub enum PropertyAccessKind { SafeDot, /// `a?:b` SafeColon, + /// 'a::b' + Scope, } impl PropertyAccessKind { @@ -265,6 +289,7 @@ impl PropertyAccessKind { PropertyAccessKind::Colon => ":", PropertyAccessKind::SafeDot => "?.", PropertyAccessKind::SafeColon => "?:", + PropertyAccessKind::Scope => "::", } } } @@ -275,12 +300,64 @@ impl fmt::Display for PropertyAccessKind { } } +/// Description of a proc's return type (`as` phrase). +#[derive(Debug, Clone, PartialEq, Eq, Hash, GetSize)] +pub enum ProcReturnType { + InputType(InputType), + TypePath(Vec), +} + +impl ProcReturnType { + pub fn is_empty(&self) -> bool { + matches!(self, ProcReturnType::InputType(InputType { bits: 0 })) + } +} + +impl Default for ProcReturnType { + fn default() -> Self { + ProcReturnType::InputType(InputType::empty()) + } +} + +/// Information about a proc declaration +/// +/// Holds what sort of decl it was (did it use /proc or /verb), alongside a set of flags +/// That describe extra info pulled from the path +#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)] +pub struct ProcDeclBuilder { + pub kind: ProcDeclKind, + pub flags: ProcFlags, +} + +impl ProcDeclBuilder { + pub fn new(kind: ProcDeclKind, flags: Option) -> ProcDeclBuilder { + ProcDeclBuilder { + kind, + flags: flags.unwrap_or_default(), + } + } + + pub fn kind(self) -> &'static str { + self.kind.name() + } + + pub fn is_final(self) -> bool { + self.flags.is_final() + } +} + +impl fmt::Display for ProcDeclBuilder { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{}{}", self.kind, self.flags) + } +} + /// The proc declaration kind, either `proc` or `verb`. /// /// DM requires referencing proc paths to include whether the target is /// declared as a proc or verb, even though the two modes are functionally /// identical in many other respects. -#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, GetSize)] pub enum ProcDeclKind { Proc, Verb, @@ -316,7 +393,48 @@ impl fmt::Display for ProcDeclKind { } } -#[derive(Debug, Clone, Copy, PartialEq)] +bitflags! { + #[derive(Default, GetSize)] + pub struct ProcFlags: u8 { + // DM flags + const FINAL = 1 << 0; + } +} + +impl ProcFlags { + pub fn from_name(name: &str) -> Option { + match name { + // DM flags + "final" => Some(ProcFlags::FINAL), + // Fallback + _ => None, + } + } + + #[inline] + pub fn is_final(&self) -> bool { + self.contains(ProcFlags::FINAL) + } + + pub fn to_vec(&self) -> Vec<&'static str> { + let mut v = Vec::new(); + if self.is_final() { + v.push("final"); + } + v + } +} + +impl fmt::Display for ProcFlags { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + if self.is_final() { + fmt.write_str("/final")?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, GetSize)] pub enum SettingMode { /// As in `set name = "Use"`. Assign, @@ -354,12 +472,20 @@ macro_rules! type_table { } impl $name { - pub fn from_str(text: &str) -> Option { - match text { + pub const ENTRIES: &'static [(&'static str, $name)] = &[ + $(($txt, $name::$i),)* + ]; + } + + impl std::str::FromStr for $name { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { $( - $txt => Some($name::$i), + $txt => Ok($name::$i), )* - _ => None, + _ => Err(()), } } } @@ -384,6 +510,7 @@ macro_rules! type_table { type_table! { /// A type specifier for verb arguments and input() calls. + #[derive(GetSize)] pub struct InputType; // These values can be known with an invocation such as: @@ -404,10 +531,48 @@ type_table! { "password", PASSWORD, 1 << 15; "command_text", COMMAND_TEXT, 1 << 16; "color", COLOR, 1 << 17; + // Non-primitive combinations that are still valid as(X) calls: + "movable", MOVABLE, Self::OBJ.bits | Self::MOB.bits; + "atom", ATOM, Self::AREA.bits | Self::TURF.bits | Self::OBJ.bits | Self::MOB.bits; + // Placeholder value for `as list` that's technically only legal as a proc return type, but whatever. + "list", LIST, 1 << 31; +} + +impl Default for InputType { + fn default() -> Self { + Self::empty() + } +} + +impl InputType { + /// Get a typepath that approximates this input type, if possible. + pub fn to_typepath(&self) -> Option<&'static str> { + if self.is_empty() { + None + } else if *self == InputType::MOB { + Some("/mob") + } else if *self == InputType::OBJ { + Some("/obj") + } else if *self == InputType::TURF { + Some("/turf") + } else if *self == InputType::AREA { + Some("/area") + } else if *self == InputType::LIST { + Some("/list") + } else if self.difference(InputType::MOVABLE).is_empty() { + // Only applies to exactly movable = mob | obj + Some("/atom/movable") + } else if self.difference(InputType::ATOM).is_empty() { + // Might apply to area|turf or turf|mob or similar combos + Some("/atom") + } else { + None + } + } } bitflags! { - #[derive(Default)] + #[derive(Default, GetSize)] pub struct VarTypeFlags: u8 { // DM flags const STATIC = 1 << 0; @@ -427,6 +592,7 @@ impl VarTypeFlags { "global" | "static" => Some(VarTypeFlags::STATIC), "const" => Some(VarTypeFlags::CONST), "tmp" => Some(VarTypeFlags::TMP), + "final" => Some(VarTypeFlags::FINAL), // SpacemanDMM flags "SpacemanDMM_final" => Some(VarTypeFlags::FINAL), "SpacemanDMM_private" => Some(VarTypeFlags::PRIVATE), @@ -468,7 +634,8 @@ impl VarTypeFlags { #[inline] pub fn is_const_evaluable(&self) -> bool { - self.contains(VarTypeFlags::CONST) || !self.intersects(VarTypeFlags::STATIC | VarTypeFlags::PROTECTED) + self.contains(VarTypeFlags::CONST) + || !self.intersects(VarTypeFlags::STATIC | VarTypeFlags::PROTECTED) } #[inline] @@ -478,12 +645,24 @@ impl VarTypeFlags { pub fn to_vec(&self) -> Vec<&'static str> { let mut v = Vec::new(); - if self.is_static() { v.push("static"); } - if self.is_const() { v.push("const"); } - if self.is_tmp() { v.push("tmp"); } - if self.is_final() { v.push("SpacemanDMM_final"); } - if self.is_private() { v.push("SpacemanDMM_private"); } - if self.is_protected() { v.push("SpacemanDMM_protected"); } + if self.is_static() { + v.push("static"); + } + if self.is_const() { + v.push("const"); + } + if self.is_tmp() { + v.push("tmp"); + } + if self.is_final() { + v.push("final"); + } + if self.is_private() { + v.push("SpacemanDMM_private"); + } + if self.is_protected() { + v.push("SpacemanDMM_protected"); + } v } } @@ -500,7 +679,7 @@ impl fmt::Display for VarTypeFlags { fmt.write_str("tmp/")?; } if self.is_final() { - fmt.write_str("SpacemanDMM_final/")?; + fmt.write_str("final/")?; } if self.is_private() { fmt.write_str("SpacemanDMM_private/")?; @@ -528,7 +707,7 @@ pub struct Ident2 { impl Ident2 { pub fn as_str(&self) -> &str { - &*self.inner + &self.inner } } @@ -559,7 +738,7 @@ impl From for String { impl std::ops::Deref for Ident2 { type Target = str; fn deref(&self) -> &str { - &*self.inner + &self.inner } } @@ -575,8 +754,14 @@ impl fmt::Debug for Ident2 { } } +impl GetSize for Ident2 { + fn get_heap_size(&self) -> usize { + self.inner.len() + } +} + /// An AST element with an additional location attached. -#[derive(Copy, Clone, Eq, Debug)] +#[derive(Copy, Clone, Eq, Debug, GetSize)] pub struct Spanned { // TODO: add a Span type and use it here pub location: Location, @@ -604,7 +789,7 @@ pub struct FormatTreePath<'a, T>(pub &'a [T]); impl<'a, T: fmt::Display> fmt::Display for FormatTreePath<'a, T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for each in self.0.iter() { - write!(f, "/{}", each)?; + write!(f, "/{each}")?; } Ok(()) } @@ -628,7 +813,7 @@ impl<'a> fmt::Display for FormatTypePath<'a> { // Terms and Expressions /// A typepath optionally followed by a set of variables. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, GetSize)] pub struct Prefab { pub path: TypePath, pub vars: Box<[(Ident2, Expression)]>, @@ -648,7 +833,7 @@ pub struct FormatVars<'a, T>(pub &'a T); impl<'a, T, K, V> fmt::Display for FormatVars<'a, T> where - &'a T: IntoIterator, + &'a T: IntoIterator, K: fmt::Display, V: fmt::Display, { @@ -666,7 +851,7 @@ where } /// The structure of an expression, a tree of terms and operators. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, GetSize)] pub enum Expression { /// An expression containing a term directly. The term is evaluated first, /// then its follows, then its unary operators in reverse order. @@ -702,14 +887,14 @@ pub enum Expression { if_: Box, /// The value otherwise. else_: Box, - } + }, } impl Expression { /// If this expression consists of a single term, return it. pub fn as_term(&self) -> Option<&Term> { match self { - &Expression::Base { ref term, ref follow } if follow.is_empty() => Some(&term.elem), + Expression::Base { term, follow } if follow.is_empty() => Some(&term.elem), _ => None, } } @@ -731,29 +916,29 @@ impl Expression { pub fn is_const_eval(&self) -> bool { match self { Expression::BinaryOp { op, lhs, rhs } => { - guard!(let Some(lhterm) = lhs.as_term() else { - return false - }); - guard!(let Some(rhterm) = rhs.as_term() else { - return false - }); + let Some(lhterm) = lhs.as_term() else { + return false; + }; + let Some(rhterm) = rhs.as_term() else { + return false; + }; if !lhterm.is_static() { - return false + return false; } if !rhterm.is_static() { - return false - } - match op { - BinaryOp::Eq | - BinaryOp::NotEq | - BinaryOp::Less | - BinaryOp::Greater | - BinaryOp::LessEq | - BinaryOp::GreaterEq | - BinaryOp::And | - BinaryOp::Or => true, - _ => false, + return false; } + matches!( + op, + BinaryOp::Eq + | BinaryOp::NotEq + | BinaryOp::Less + | BinaryOp::Greater + | BinaryOp::LessEq + | BinaryOp::GreaterEq + | BinaryOp::And + | BinaryOp::Or + ) }, _ => false, } @@ -762,9 +947,7 @@ impl Expression { pub fn is_truthy(&self) -> Option { match self { Expression::Base { term, follow } => { - guard!(let Some(mut truthy) = term.elem.is_truthy() else { - return None; - }); + let mut truthy = term.elem.is_truthy()?; for follow in follow.iter() { match follow.elem { Follow::Unary(UnaryOp::Not) => truthy = !truthy, @@ -774,12 +957,8 @@ impl Expression { Some(truthy) }, Expression::BinaryOp { op, lhs, rhs } => { - guard!(let Some(lhtruth) = lhs.is_truthy() else { - return None - }); - guard!(let Some(rhtruth) = rhs.is_truthy() else { - return None - }); + let lhtruth = lhs.is_truthy()?; + let rhtruth = rhs.is_truthy()?; match op { BinaryOp::And => Some(lhtruth && rhtruth), BinaryOp::Or => Some(lhtruth || rhtruth), @@ -788,24 +967,35 @@ impl Expression { }, Expression::AssignOp { op, lhs: _, rhs } => { if let AssignOp::Assign = op { - return match rhs.as_term() { + match rhs.as_term() { Some(term) => term.is_truthy(), _ => None, } } else { - return None + None } }, Expression::TernaryOp { cond, if_, else_ } => { - guard!(let Some(condtruth) = cond.is_truthy() else { - return None - }); + let condtruth = cond.is_truthy()?; if condtruth { if_.is_truthy() } else { else_.is_truthy() } - } + }, + } + } + + pub fn nameof(&self) -> Option<&str> { + match self { + Expression::Base { term, follow } => { + if let Some(last) = follow.last() { + last.elem.nameof() + } else { + term.elem.nameof() + } + }, + _ => None, } } } @@ -823,7 +1013,8 @@ impl From for Expression { } /// The structure of a term, the basic building block of the AST. -#[derive(Clone, PartialEq, Debug)] +#[allow(non_camel_case_types)] +#[derive(Clone, PartialEq, Debug, GetSize)] pub enum Term { // Terms with no recursive contents --------------------------------------- /// The literal `null`. @@ -840,6 +1031,15 @@ pub enum Term { Resource(String), /// An `as()` call, with an input type. Undocumented. As(InputType), + /// A reference to our current proc's name + __PROC__, + /// A reference to the current proc/scope's type + __TYPE__, + /// If rhs of an assignment op, this is a reference to the lhs var's type + /// If we're used as the second arg of an istype then it's the implied type of the first arg + /// Second case takes precedence over the first, but we don't properly implement because it would be impossible to + /// Tell. You can't DO anything to the __IMPLIED_TYPE__ so we don't really need to care about it + __IMPLIED_TYPE__, // Non-function calls with recursive contents ----------------------------- /// An expression contained in a term. @@ -880,7 +1080,7 @@ pub enum Term { /// An `input` call. Input { args: Box<[Expression]>, - input_type: Option, // as + input_type: Option, // as in_list: Option>, // in }, /// A `locate` call. @@ -889,24 +1089,31 @@ pub enum Term { in_list: Option>, // in }, /// A `pick` call, possibly with weights. - Pick(Box<[(Option, Expression)]>), + Pick(Box), /// A use of the `call()()` primitive. DynamicCall(Box<[Expression]>, Box<[Expression]>), + /// A use of the `call_ext()()` primitive. + ExternalCall { + library: Option>, + function: Box, + args: Box<[Expression]>, + }, + /// Unscoped `::A` is a shorthand for `global.A` + GlobalIdent(Ident2), + /// Unscoped `::A(...)` is a shorthand for `global.A(...)` + GlobalCall(Ident2, Box<[Expression]>), } impl Term { pub fn is_static(&self) -> bool { - matches!(self, - Term::Null - | Term::Int(_) - | Term::Float(_) - | Term::String(_) - | Term::Prefab(_) + matches!( + self, + Term::Null | Term::Int(_) | Term::Float(_) | Term::String(_) | Term::Prefab(_) ) } pub fn is_truthy(&self) -> Option { - return match self { + match self { // `null`, `0`, and empty strings are falsey. Term::Null => Some(false), Term::Int(i) => Some(*i != 0), @@ -935,7 +1142,7 @@ impl Term { Term::Expr(e) => e.is_truthy(), _ => None, - }; + } } pub fn valid_for_range(&self, other: &Term, step: Option<&Expression>) -> Option { @@ -943,48 +1150,60 @@ impl Term { if let Term::Int(o) = *other { // edge case if i == 0 && o == 0 { - return Some(false) + return Some(false); } if let Some(stepexp) = step { if let Some(stepterm) = stepexp.as_term() { if let Term::Int(_s) = stepterm { - return Some(true) + return Some(true); } } else { - return Some(true) + return Some(true); } } - return Some(i <= o) + return Some(i <= o); } } None } + + pub fn nameof(&self) -> Option<&str> { + match self { + Term::Expr(e) => e.nameof(), + Term::Ident(i) => Some(i), + Term::Prefab(fab) if fab.vars.is_empty() => Some(&fab.path.last()?.1), + Term::GlobalIdent(i) => Some(i), + _ => None, + } + } } impl From for Term { fn from(expr: Expression) -> Term { match expr { - Expression::Base { term, follow } => if follow.is_empty() { - match term.elem { - Term::Expr(expr) => Term::from(*expr), - other => other, + Expression::Base { term, follow } => { + if follow.is_empty() { + match term.elem { + Term::Expr(expr) => Term::from(*expr), + other => other, + } + } else { + Term::Expr(Box::new(Expression::Base { term, follow })) } - } else { - Term::Expr(Box::new(Expression::Base { term, follow })) }, other => Term::Expr(Box::new(other)), } } } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, GetSize)] pub struct MiniExpr { pub ident: Ident2, pub fields: Box<[Field]>, } /// An expression part which is applied to a term or another follow. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, GetSize)] pub enum Follow { /// Index the value by an expression. Index(ListAccessKind, Box), @@ -994,10 +1213,31 @@ pub enum Follow { Call(PropertyAccessKind, Ident2, Box<[Expression]>), /// Apply a unary operator to the value. Unary(UnaryOp), + /// Any of: + /// - `/typepath::static_var` to read/write any type's static variables. + /// - `/typepath::normal_var` gets the initial value of any type var. + /// - `parent_type::normal_var` gets the initial value on the parent type. Only works outside procs. + /// - `type::normal_var` gets the initial value on the current type. Only works outside procs. Beware loops. + StaticField(Ident2), + /// `foo::bar()` is a proc reference. + /// If the LHS is a constant typepath, that is used. + /// Otherwise the **static** type of LHS is used. + ProcReference(Ident2), +} + +impl Follow { + pub fn nameof(&self) -> Option<&str> { + match self { + Follow::Field(_, i) => Some(i), + Follow::StaticField(i) => Some(i), + Follow::ProcReference(i) => Some(i), + _ => None, + } + } } /// Like a `Follow` but only supports field accesses. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, GetSize)] pub struct Field { pub kind: PropertyAccessKind, pub ident: Ident2, @@ -1010,7 +1250,7 @@ impl From for Follow { } /// A parameter declaration in the header of a proc. -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, GetSize)] pub struct Parameter { pub var_type: VarType, pub name: Ident, @@ -1024,17 +1264,18 @@ impl fmt::Display for Parameter { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!(fmt, "{}{}", self.var_type, self.name)?; if let Some(input_type) = self.input_type { - write!(fmt, " as {}", input_type)?; + write!(fmt, " as {input_type}")?; } Ok(()) } } /// A type which may be ascribed to a `var`. -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, GetSize)] pub struct VarType { pub flags: VarTypeFlags, pub type_path: TreePath, + pub input_type: InputType, } impl VarType { @@ -1050,7 +1291,7 @@ impl VarType { } impl FromIterator for VarType { - fn from_iter>(iter: T) -> Self { + fn from_iter>(iter: T) -> Self { VarTypeBuilder::from_iter(iter).build() } } @@ -1071,6 +1312,7 @@ impl fmt::Display for VarType { pub struct VarTypeBuilder { pub flags: VarTypeFlags, pub type_path: Vec, + pub input_type: Option, } impl VarTypeBuilder { @@ -1084,12 +1326,13 @@ impl VarTypeBuilder { VarType { flags: self.flags, type_path: self.type_path.into_boxed_slice(), + input_type: self.input_type.unwrap_or_default(), } } } impl FromIterator for VarTypeBuilder { - fn from_iter>(iter: T) -> Self { + fn from_iter>(iter: T) -> Self { let mut flags = VarTypeFlags::default(); let type_path = iter .into_iter() @@ -1105,6 +1348,7 @@ impl FromIterator for VarTypeBuilder { VarTypeBuilder { flags, type_path, + input_type: None, } } } @@ -1124,7 +1368,7 @@ impl VarSuffix { pub fn into_initializer(self) -> Option { // `var/L[10]` is equivalent to `var/list/L = new /list(10)` // `var/L[2][][3]` is equivalent to `var/list/list/list = new /list(2, 3)` - let args: Vec<_> = self.list.into_iter().filter_map(|x| x).collect(); + let args: Vec<_> = self.list.into_iter().flatten().collect(); if args.is_empty() { None } else { @@ -1143,7 +1387,7 @@ impl VarSuffix { pub type Block = Box<[Spanned]>; /// A statement in a proc body. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, GetSize)] pub enum Statement { Expr(Expression), Return(Option), @@ -1158,7 +1402,7 @@ pub enum Statement { }, If { arms: Vec<(Spanned, Block)>, - else_arm: Option + else_arm: Option, }, ForInfinite { block: Block, @@ -1170,13 +1414,14 @@ pub enum Statement { block: Block, }, ForList(Box), + ForKeyValue(Box), ForRange(Box), Var(Box), Vars(Vec), Setting { name: Ident2, mode: SettingMode, - value: Expression + value: Expression, }, Spawn { delay: Option, @@ -1184,7 +1429,7 @@ pub enum Statement { }, Switch { input: Box, - cases: Box<[(Spanned>, Block)]>, + cases: Box, default: Option, }, TryCatch { @@ -1203,20 +1448,20 @@ pub enum Statement { Crash(Option), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, GetSize)] pub struct VarStatement { pub var_type: VarType, pub name: Ident, pub value: Option, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, GetSize)] pub enum Case { Exact(Expression), Range(Expression, Expression), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, GetSize)] pub struct ForListStatement { pub var_type: Option, pub name: Ident2, @@ -1227,7 +1472,18 @@ pub struct ForListStatement { pub block: Block, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, GetSize)] +pub struct ForKeyValueStatement { + pub var_type: Option, + pub key: Ident2, + pub key_input_type: Option, + pub value: Ident2, + /// Defaults to 'world'. + pub in_list: Option, + pub block: Block, +} + +#[derive(Debug, Clone, PartialEq, GetSize)] pub struct ForRangeStatement { pub var_type: Option, pub name: Ident2, @@ -1259,7 +1515,7 @@ pub static VALID_FILTER_TYPES: phf::Map<&'static str, &[&str]> = phf_map! { "angular_blur" => &[ "x", "y", "size" ], "bloom" => &[ "threshold", "size", "offset", "alpha" ], "color" => &[ "color", "space" ], - "displace" => &[ "x", "y", "size", "icon", "render_source" ], + "displace" => &[ "x", "y", "size", "icon", "render_source", "flags" ], "drop_shadow" => &[ "x", "y", "size", "offset", "color"], "blur" => &[ "size" ], "layer" => &[ "x", "y", "icon", "render_source", "flags", "color", "transform", "blend_mode" ], @@ -1275,9 +1531,16 @@ pub static VALID_FILTER_TYPES: phf::Map<&'static str, &[&str]> = phf_map! { pub static VALID_FILTER_FLAGS: phf::Map<&'static str, (&str, bool, bool, &[&str])> = phf_map! { "alpha" => ("flags", false, true, &[ "MASK_INVERSE", "MASK_SWAP" ]), "color" => ("space", true, false, &[ "FILTER_COLOR_RGB", "FILTER_COLOR_HSV", "FILTER_COLOR_HSL", "FILTER_COLOR_HCY" ]), + "displace" => ("flags", false, true, &[ "FILTER_OVERLAY" ]), "layer" => ("flags", true, true, &[ "FILTER_OVERLAY", "FILTER_UNDERLAY" ]), "rays" => ("flags", false, true, &[ "FILTER_OVERLAY", "FILTER_UNDERLAY" ]), "outline" => ("flags", false, true, &[ "OUTLINE_SHARP", "OUTLINE_SQUARE" ]), "ripple" => ("flags", false, true, &[ "WAVE_BOUNDED" ]), "wave" => ("flags", false, true, &[ "WAVE_SIDEWAYS", "WAVE_BOUNDED" ]), }; + +// ---------------------------------------------------------------------------- +// Guard against sizeof regression. +const _: [(); 0 - !(std::mem::size_of::() <= 56) as usize] = []; +const _: [(); 0 - !(std::mem::size_of::() <= 32) as usize] = []; +const _: [(); 0 - !(std::mem::size_of::() <= 40) as usize] = []; diff --git a/crates/dreammaker/src/builtins.rs b/crates/dreammaker/src/builtins.rs index c17c164d..d76042ee 100644 --- a/crates/dreammaker/src/builtins.rs +++ b/crates/dreammaker/src/builtins.rs @@ -2,64 +2,76 @@ use builtins_proc_macro::builtins_table; -use super::objtree::*; -use super::Location; -use super::preprocessor::{DefineMap, Define}; use super::constants::Constant; use super::docs::{BuiltinDocs, DocCollection}; +use super::objtree::*; +use super::preprocessor::{Define, DefineMap}; +use super::Location; -const DM_VERSION: i32 = 514; -const DM_BUILD: i32 = 1556; +const DM_VERSION: i32 = 516; +const DM_BUILD: i32 = 1666; /// Register BYOND builtin macros to the given define map. pub fn default_defines(defines: &mut DefineMap) { - use super::lexer::*; use super::lexer::Token::*; + use super::lexer::*; let location = Location::builtins(); // #define EXCEPTION(value) new /exception(value) - defines.insert("EXCEPTION".to_owned(), (location, Define::Function { - params: vec!["value".to_owned()], - variadic: false, - subst: vec![ - Ident("new".to_owned(), true), - Punct(Punctuation::Slash), - Ident("exception".to_owned(), false), - Punct(Punctuation::LParen), - Ident("value".to_owned(), false), - Punct(Punctuation::RParen), - ], - docs: Default::default(), - })); + defines.insert( + "EXCEPTION".to_owned(), + ( + location, + Define::Function { + params: vec!["value".to_owned()], + variadic: false, + subst: vec![ + Ident("new".to_owned(), true), + Punct(Punctuation::Slash), + Ident("exception".to_owned(), false), + Punct(Punctuation::LParen), + Ident("value".to_owned(), false), + Punct(Punctuation::RParen), + ], + docs: Default::default(), + }, + ), + ); // #define ASSERT(expression) if (!(expression)) { CRASH("[__FILE__]:[__LINE__]:Assertion Failed: [#X]") } - defines.insert("ASSERT".to_owned(), (location, Define::Function { - params: vec!["expression".to_owned()], - variadic: false, - subst: vec![ - Ident("if".to_owned(), true), - Punct(Punctuation::LParen), - Punct(Punctuation::Not), - Punct(Punctuation::LParen), - Ident("expression".to_owned(), false), - Punct(Punctuation::RParen), - Punct(Punctuation::RParen), - Punct(Punctuation::LBrace), - Ident("CRASH".to_owned(), false), - Punct(Punctuation::LParen), - InterpStringBegin("".to_owned()), - Ident("__FILE__".to_owned(), false), - InterpStringPart(":".to_owned()), - Ident("__LINE__".to_owned(), false), - InterpStringPart(":Assertion Failed: ".to_owned()), - Punct(Punctuation::Hash), - Ident("expression".to_owned(), false), - InterpStringEnd("".to_owned()), - Punct(Punctuation::RParen), - Punct(Punctuation::RBrace), - ], - docs: Default::default(), - })); + defines.insert( + "ASSERT".to_owned(), + ( + location, + Define::Function { + params: vec!["expression".to_owned()], + variadic: false, + subst: vec![ + Ident("if".to_owned(), true), + Punct(Punctuation::LParen), + Punct(Punctuation::Not), + Punct(Punctuation::LParen), + Ident("expression".to_owned(), false), + Punct(Punctuation::RParen), + Punct(Punctuation::RParen), + Punct(Punctuation::LBrace), + Ident("CRASH".to_owned(), false), + Punct(Punctuation::LParen), + InterpStringBegin("".to_owned()), + Ident("__FILE__".to_owned(), false), + InterpStringPart(":".to_owned()), + Ident("__LINE__".to_owned(), false), + InterpStringPart(":Assertion Failed: ".to_owned()), + Punct(Punctuation::Hash), + Ident("expression".to_owned(), false), + InterpStringEnd("".to_owned()), + Punct(Punctuation::RParen), + Punct(Punctuation::RBrace), + ], + docs: Default::default(), + }, + ), + ); // constants macro_rules! c { @@ -148,7 +160,10 @@ pub fn default_defines(defines: &mut DefineMap) { ANIMATION_END_NOW = Int(1); ANIMATION_LINEAR_TRANSFORM = Int(2); ANIMATION_PARALLEL = Int(4); + ANIMATION_SLICE = Int(8); // 515 + ANIMATION_END_LOOP = Int(16); // 516 ANIMATION_RELATIVE = Int(256); + ANIMATION_CONTINUE = Int(512); // 515 // database DATABASE_OPEN = Int(0); @@ -195,6 +210,14 @@ pub fn default_defines(defines: &mut DefineMap) { NORMAL_RAND = Int(1); LINEAR_RAND = Int(2); SQUARE_RAND = Int(3); + + + // json encode flags (515) + JSON_PRETTY_PRINT = Int(1); + + // json decode flags (515) + JSON_STRICT = Int(1); + JSON_ALLOW_COMMENTS = Int(2); // default } } @@ -393,7 +416,7 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { proc/abs(A); proc/addtext(Arg1, Arg2/*, ...*/); proc/alert(Usr/*=usr*/,Message,Title,Button1/*="Ok"*/,Button2,Button3); - proc/animate(Object, time, loop, easing, flags, // +2 forms + proc/animate(Object, time, loop, easing, flags, delay, tag, command, // +2 forms // these kwargs alpha, color, infra_luminosity, layer, maptext_width, maptext_height, maptext_x, maptext_y, luminosity, pixel_x, pixel_y, pixel_w, pixel_z, @@ -441,7 +464,8 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { repeat, radius, falloff, - alpha + alpha, + name // 516 ); proc/findlasttext(Haystack,Needle,Start=0,End=1); proc/findlasttextEx(Haystack,Needle,Start=0,End=1); @@ -457,14 +481,14 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { proc/get_step_rand(Ref); proc/get_step_to(Ref,Trg,Min=0); proc/get_step_towards(Ref,Trg); - proc/gradient(Gradient, index); // unsure how to handle (Item1, Item2, ..., index) form + proc/gradient(Gradient, index, space = COLORSPACE_RGB); // unsure how to handle (Item1, Item2, ..., index) form proc/hascall(Object,ProcName); proc/hearers(Depth=world.view,Center=usr); proc/html_decode(HtmlText); proc/html_encode(PlainText); proc/icon(icon,icon_state,dir,frame,moving); // SNA proc/icon_states(Icon, mode=0); - proc/image(icon,loc,icon_state,layer,dir,pixel_x,pixel_y); // SNA + proc/image(icon,loc,icon_state,layer,dir,pixel_x,pixel_y,pixel_w,pixel_z); // SNA proc/initial(Var); // special form proc/input(Usr=usr,Message,Title,Default)/*as Type in List*/; // special form proc/isarea(Loc1, Loc2/*,...*/); @@ -566,6 +590,12 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { proc/winshow(player, window, show=1); proc/CRASH(message); // kind of special, but let's pretend + proc/values_cut_over(Alist, Max, inclusive=0); + proc/values_cut_under(Alist, Max, inclusive=0); + proc/values_dot(A, B); + proc/values_product(Alist); + proc/values_sum(Alist); + // database builtin procs proc/_dm_db_new_query(); proc/_dm_db_execute(db_query, sql_query, db_connection, cursor_handler, unknown); @@ -590,7 +620,7 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { savefile yes yes yes yes yes yes client yes yes yes yes yes yes yes - All other root types have an implicit `parent_type = /datum`. + Most other root types have an implicit `parent_type = /datum`. */ datum; datum/var/const/type; // not editable @@ -608,6 +638,7 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { list/var/const/parent_type; list/var/tag; list/var/const/list/vars; + list/proc/operator[](); list/proc/Add(Item1, Item2/*,...*/); list/proc/Copy(Start=1, End=0); list/proc/Cut(Start=1, End=0); @@ -619,6 +650,23 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { list/proc/Swap(Index1, Index2); list/var/len; + // 516 + alist; + alist/var/const/type; + alist/var/const/parent_type; + alist/var/tag; + alist/proc/operator[](); + alist/proc/Add(Item1, Item2/*,...*/); + alist/proc/Copy(Start=1, End=0); + alist/proc/Cut(Start=1, End=0); + alist/proc/Find(Elem, Start=1, End=0); + alist/proc/Insert(Index, Item1, Item2/*,...*/); + alist/proc/Join(Glue, Start=1, End=0); + alist/proc/Remove(Item1, Item2/*,...*/); + alist/proc/Splice(Start=1, End=0, Item1, Item2/*,...*/); + alist/proc/Swap(Index1, Index2); + alist/var/len; + atom/parent_type = path!(/datum); atom/var/alpha = int!(255); atom/var/tmp/appearance; // not editable @@ -656,6 +704,12 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { atom/var/pixel_y = int!(0); atom/var/pixel_w = int!(0); atom/var/pixel_z = int!(0); + + // 516 + atom/var/icon_w = int!(0); + atom/var/icon_z = int!(0); + atom/var/pixloc/pixloc; + atom/var/plane = int!(0); atom/var/suffix; atom/var/text; @@ -903,6 +957,11 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { // only used by client.SoundQuery() for now: sound/var/offset = int!(0); sound/var/len = int!(0); + + // 516 + sound/var/tmp/atom/atom; + sound/var/transform; + sound/New(file, repeat, wait, channel, volume); icon; @@ -960,12 +1019,14 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { regex/proc/Replace(text, rep, start, end); database; + database/var/_binobj; database/proc/Close(); database/proc/Error(); database/proc/ErrorMsg(); database/proc/Open(filename); database/proc/New(filename); + database/query/var/database/database; database/query/proc/Add(text, item1, item2 /*...*/); database/query/proc/Close(); database/query/proc/Columns(column); @@ -1003,6 +1064,11 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { image/var/pixel_y; image/var/pixel_w; image/var/pixel_z; + + // 516 + image/var/icon_w; + image/var/icon_z; + image/var/plane; image/var/render_source; image/var/render_target; @@ -1058,12 +1124,41 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { savefile/var/list/dir; savefile/var/eof; savefile/var/name; + savefile/proc/operator[](); savefile/proc/ExportText(/* path=cd, file */); savefile/proc/Flush(); savefile/proc/ImportText(/* path=cd, file */); savefile/proc/Lock(timeout); savefile/proc/Unlock(); + //512 stuff + + // /dm_filter is a hidden type that can be used to manipulate filter + // instances without using the runtime search operator (:). It does + // not descend from datum, cannot be subtyped, and can only be created + // successfully by a valid call to proc/filter(...). All filter types + // create the same kind of /dm_filter, but with different properties. + dm_filter; + dm_filter/var/const/type; + dm_filter/var/x; + dm_filter/var/y; + dm_filter/var/icon; + dm_filter/var/render_source; + dm_filter/var/flags; + dm_filter/var/size; + dm_filter/var/threshold; + dm_filter/var/offset; + dm_filter/var/alpha; + dm_filter/var/color; + dm_filter/var/space; + dm_filter/var/transform; + dm_filter/var/blend_mode; + dm_filter/var/density; + dm_filter/var/factor; + dm_filter/var/repeat; + dm_filter/var/radius; + dm_filter/var/falloff; + // 513 stuff proc/arctan(A,B); proc/clamp(NumberOrList,Low,High); @@ -1100,6 +1195,7 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { // 514 stuff generator; + generator/var/_binobj; generator/proc/Rand(); generator/proc/Turn(a); @@ -1130,6 +1226,83 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) { particles/var/rotation; particles/var/spin; particles/var/drift; + + //515 stuff + + proc/ceil(A); + proc/floor(A); + proc/fract(A); + proc/ftime(File, IsCreationTime); + proc/get_steps_to(Ref, Trg, Min=0); + proc/isinf(A); + proc/isnan(A); + proc/ispointer(Value); + proc/nameof(VarPathProcRef); + proc/noise_hash(param1/*, ...*/); + proc/refcount(Object); + proc/trimtext(Text); + proc/trunc(A); + proc/bound_pixloc(Atom, Dir); + + client/proc/RenderIcon(object); + + savefile/var/byond_build = int!(0); + savefile/var/byond_version = int!(0); + + + sound/var/params; + sound/var/pitch = int!(0); + + list/proc/RemoveAll(Item1/*, ...*/); + + world/proc/Tick(); + + // 516 + proc/lerp(A, B, factor); + proc/sign(A); + proc/astype(Val, Type); + proc/alist(A/* =a */,B/* =b */,C/* =c */); + + proc/load_ext(LibName, FuncName); + + callee; + callee/var/args; + callee/var/callee/caller; + callee/var/category; + callee/var/desc; + callee/var/file; + callee/var/name; + callee/var/line; + callee/var/proc; + callee/var/src; + callee/var/type; + callee/var/usr; + + proc/pixloc(x, y, z); + + pixloc; + pixloc/var/atom/loc; + pixloc/var/step_x; + pixloc/var/step_y; + pixloc/var/x; + pixloc/var/y; + pixloc/var/z; + + proc/vector(x, y, z); + + vector; + vector/var/len; + vector/var/size; + vector/var/x; + vector/var/y; + vector/var/z; + + vector/proc/operator[](); + vector/proc/Cross(B); + vector/proc/Dot(B); + vector/proc/Interpolate(B, t); + vector/proc/Normalize(); + vector/proc/Turn(B); }; } diff --git a/crates/dreammaker/src/config.rs b/crates/dreammaker/src/config.rs index 1f129c50..c4668c53 100644 --- a/crates/dreammaker/src/config.rs +++ b/crates/dreammaker/src/config.rs @@ -1,11 +1,10 @@ //! Configuration file for diagnostics. +use foldhash::HashMap; use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; -use std::collections::HashMap; -use ahash::RandomState; use serde::Deserialize; use crate::error::Severity; @@ -19,7 +18,7 @@ pub struct Config { // diagnostic configuration display: WarningDisplay, - diagnostics: HashMap, + diagnostics: HashMap, pub code_standards: CodeStandards, // tool-specific configuration @@ -69,6 +68,7 @@ pub struct Debugger { /// Severity overrides from configuration #[derive(Debug, Deserialize, Clone, Copy, PartialEq)] #[serde(rename_all(deserialize = "lowercase"))] +#[derive(Default)] pub enum WarningLevel { #[serde(alias = "errors")] Error = 1, @@ -80,15 +80,17 @@ pub enum WarningLevel { Hint = 4, #[serde(alias = "false", alias = "off")] Disabled = 5, + #[default] Unset = 6, } /// Available debug engines. -#[derive(Debug, Deserialize, Clone, Copy, PartialEq)] +#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq)] pub enum DebugEngine { #[serde(alias = "extools")] Extools, #[serde(alias = "auxtools")] + #[default] Auxtools, } @@ -99,10 +101,10 @@ pub struct MapRenderer { /// Map from render pass name to whether it should be enabled/disabled. /// /// Priority is: CLI arguments > config > defaults. - pub render_passes: HashMap, + pub render_passes: HashMap, /// Map from typepath to layer number. - pub fancy_layers: HashMap, + pub fancy_layers: HashMap, /// List of typepath to just hide pub hide_invisible: Vec, @@ -121,7 +123,7 @@ impl Config { fn config_warninglevel(&self, error: &DMError) -> Option<&WarningLevel> { if let Some(errortype) = error.errortype() { - return self.diagnostics.get(errortype) + return self.diagnostics.get(errortype); } None } @@ -161,12 +163,6 @@ impl WarningLevel { } } -impl Default for WarningLevel { - fn default() -> WarningLevel { - WarningLevel::Unset - } -} - impl From for WarningLevel { fn from(severity: Severity) -> Self { match severity { @@ -180,19 +176,13 @@ impl From for WarningLevel { impl PartialEq for WarningLevel { fn eq(&self, other: &Severity) -> bool { - match (self, other) { - (WarningLevel::Error, Severity::Error) => true, - (WarningLevel::Warning, Severity::Warning) => true, - (WarningLevel::Info, Severity::Info) => true, - (WarningLevel::Hint, Severity::Hint) => true, - _ => false, - } - } -} - -impl Default for DebugEngine { - fn default() -> Self { - Self::Extools + matches!( + (self, other), + (WarningLevel::Error, Severity::Error) + | (WarningLevel::Warning, Severity::Warning) + | (WarningLevel::Info, Severity::Info) + | (WarningLevel::Hint, Severity::Hint) + ) } } diff --git a/crates/dreammaker/src/constants.rs b/crates/dreammaker/src/constants.rs index bd9959c9..1708f8ad 100644 --- a/crates/dreammaker/src/constants.rs +++ b/crates/dreammaker/src/constants.rs @@ -3,30 +3,49 @@ use std::fmt; use std::ops; use std::path::Path; -use indexmap::IndexMap; -use ahash::RandomState; -use ordered_float::OrderedFloat; +use get_size::GetSize; +use get_size_derive::GetSize; + use color_space::{Hsl, Hsv, Lch, Rgb}; +use foldhash::fast::RandomState; +use indexmap::IndexMap; +use ordered_float::OrderedFloat; + +use crate::heap_size_of_index_map; use super::ast::*; use super::objtree::*; use super::preprocessor::DefineMap; use super::{Context, DMError, HasLocation, Location, Severity}; +pub type Arguments = [(Constant, Option)]; + /// An absolute typepath and optional variables. /// /// The path may involve `/proc` or `/verb` references. -#[derive(Clone, Eq, PartialEq, Debug)] +#[derive(Clone, Debug, GetSize)] pub struct Pop { pub path: TreePath, + #[get_size(size_fn = heap_size_of_index_map)] pub vars: IndexMap, } +impl PartialEq for Pop { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && self.vars == other.vars + } +} + +impl Eq for Pop {} impl std::hash::Hash for Pop { fn hash(&self, state: &mut H) { self.path.hash(state); - self.vars.keys().for_each(|key| {key.hash(state)}); + let mut items: Vec<_> = self.vars.iter().collect(); + items.sort_by_key(|&(k, _)| k); + for kvp in items { + kvp.hash(state); + } } } @@ -41,7 +60,12 @@ impl From for Pop { impl fmt::Display for Pop { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", FormatTreePath(&self.path), FormatVars(&self.vars)) + write!( + f, + "{}{}", + FormatTreePath(&self.path), + FormatVars(&self.vars) + ) } } @@ -49,7 +73,7 @@ impl fmt::Display for Pop { /// /// This is intended to represent the degree to which constants are evaluated /// before being displayed in DreamMaker. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, GetSize)] pub enum Constant { /// The literal `null`. Null(Option), @@ -58,18 +82,18 @@ pub enum Constant { /// The type to be instantiated. type_: Option>, /// The list of arugments to pass to the `New()` proc. - args: Option)]>>, + args: Option>, }, /// A `list` literal. Elements have optional associations. - List(Box<[(Constant, Option)]>), + List(Box), /// A call to a constant type constructor. - Call(ConstFn, Box<[(Constant, Option)]>), + Call(ConstFn, Box), /// A prefab literal. Prefab(Box), /// A string literal. - String(Box), + String(Ident2), /// A resource literal. - Resource(Box), + Resource(Ident2), /// A floating-point (or integer) literal, following BYOND's rules. Float(f32), } @@ -104,7 +128,16 @@ impl std::cmp::PartialEq for Constant { fn eq(&self, other: &Constant) -> bool { match (self, other) { (Constant::Null(p1), Constant::Null(p2)) => p1 == p2, - (Constant::New { type_: type1, args: args1 }, Constant::New { type_: type2, args: args2 }) => (type1, args1) == (type2, args2), + ( + Constant::New { + type_: type1, + args: args1, + }, + Constant::New { + type_: type2, + args: args2, + }, + ) => (type1, args1) == (type2, args2), (Constant::List(l1), Constant::List(l2)) => l1 == l2, (Constant::Call(f1, args1), Constant::Call(f2, args2)) => (f1, args1) == (f2, args2), (Constant::Prefab(pop1), Constant::Prefab(pop2)) => pop1 == pop2, @@ -119,7 +152,7 @@ impl std::cmp::PartialEq for Constant { impl std::cmp::Eq for Constant {} /// The constant functions which are represented as-is. -#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, GetSize)] pub enum ConstFn { /// The `icon()` type constructor. Icon, @@ -144,13 +177,13 @@ impl Constant { // ------------------------------------------------------------------------ // Constructors - pub fn null() -> &'static Constant { - static NULL: Constant = Constant::Null(None); + pub const fn null<'a>() -> &'a Constant { + const NULL: Constant = Constant::Null(None); &NULL } #[inline] - pub fn string>>(s: S) -> Constant { + pub fn string>(s: S) -> Constant { Constant::String(s.into()) } @@ -194,8 +227,7 @@ impl Constant { pub fn as_path_str(&self) -> Option<&str> { match *self { - Constant::String(ref s) | - Constant::Resource(ref s) => Some(s), + Constant::String(ref s) | Constant::Resource(ref s) => Some(s), _ => None, } } @@ -217,8 +249,7 @@ impl Constant { pub fn eq_resource(&self, resource: &str) -> bool { match self { - Constant::String(ref s) | - Constant::Resource(ref s) => &**s == resource, + Constant::String(ref s) | Constant::Resource(ref s) => &**s == resource, _ => false, } } @@ -228,7 +259,7 @@ impl Constant { pub fn contains_key(&self, key: &Constant) -> bool { if let Constant::List(ref elements) = *self { - for &(ref k, _) in elements.iter() { + for (k, _) in elements.iter() { if key == k { return true; } @@ -240,13 +271,17 @@ impl Constant { pub fn index(&self, key: &Constant) -> Option<&Constant> { match (self, key) { // Narrowing conversion is intentional. - (&Constant::List(ref elements), &Constant::Float(i)) => return elements.get(i as usize).map(|&(ref k, _)| k), - (&Constant::List(ref elements), key) => for &(ref k, ref v) in elements.iter() { - if key == k { - return v.as_ref(); + (Constant::List(elements), &Constant::Float(i)) => { + return elements.get(i as usize).map(|(k, _)| k) + }, + (Constant::List(elements), key) => { + for (k, v) in elements.iter() { + if key == k { + return v.as_ref(); + } } }, - _ => {} + _ => {}, } None } @@ -289,8 +324,7 @@ impl From for Constant { impl PartialEq for Constant { fn eq(&self, other: &str) -> bool { match self { - Constant::String(ref s) | - Constant::Resource(ref s) => &**s == other, + Constant::String(ref s) | Constant::Resource(ref s) => &**s == other, _ => false, } } @@ -304,7 +338,7 @@ impl ops::Not for Constant { } } -impl<'a> ops::Not for &'a Constant { +impl ops::Not for &Constant { type Output = Constant; fn not(self) -> Constant { @@ -316,10 +350,13 @@ impl fmt::Display for Constant { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Constant::Null(_) => f.write_str("null"), - Constant::New { ref type_, ref args } => { + Constant::New { + ref type_, + ref args, + } => { f.write_str("new")?; if let Some(prefab) = type_ { - write!(f, " {}", prefab)?; + write!(f, " {prefab}")?; } if let Some(args) = args.as_ref() { write!(f, "(")?; @@ -331,7 +368,7 @@ impl fmt::Display for Constant { first = false; write!(f, "{}", each.0)?; if let Some(val) = each.1.as_ref() { - write!(f, " = {}", val)?; + write!(f, " = {val}")?; } } write!(f, ")")?; @@ -342,7 +379,7 @@ impl fmt::Display for Constant { write!(f, "list(")?; let mut first = true; let mut previous_assoc = false; - for &(ref key, ref val) in list.iter() { + for (key, val) in list.iter() { if !first { write!(f, ",")?; if previous_assoc { @@ -351,32 +388,32 @@ impl fmt::Display for Constant { } first = false; previous_assoc = false; - write!(f, "{}", key)?; + write!(f, "{key}")?; if let Some(val) = val.as_ref() { - write!(f, " = {}", val)?; + write!(f, " = {val}")?; previous_assoc = true; } } write!(f, ")") }, Constant::Call(const_fn, ref list) => { - write!(f, "{}(", const_fn)?; + write!(f, "{const_fn}(")?; let mut first = true; for (key, val) in list.iter() { if !first { write!(f, ", ")?; } first = false; - write!(f, "{}", key)?; + write!(f, "{key}")?; if let Some(val) = val { - write!(f, " = {}", val)?; + write!(f, " = {val}")?; } } write!(f, ")") }, - Constant::Prefab(ref val) => write!(f, "{}", val), + Constant::Prefab(ref val) => write!(f, "{val}"), Constant::String(ref val) => crate::lexer::Quote(val).fmt(f), - Constant::Resource(ref val) => write!(f, "'{}'", val), + Constant::Resource(ref val) => write!(f, "'{val}'"), Constant::Float(val) => crate::lexer::FormatFloat(val).fmt(f), } } @@ -400,14 +437,21 @@ impl fmt::Display for ConstFn { // The constant evaluator pub fn evaluate_str(location: Location, input: &[u8]) -> Result { - use super::lexer::{Lexer, from_utf8_or_latin1_borrowed}; + use super::lexer::{from_utf8_or_latin1_borrowed, Lexer, LocationTracker}; let ctx = Context::default(); - let mut lexer = Lexer::new(&ctx, location.file, input); + let mut lexer = Lexer::from_input(&ctx, LocationTracker::from_location(location, input.into())); let expr = crate::parser::parse_expression(&ctx, location, &mut lexer)?; let leftover = lexer.remaining(); if !leftover.is_empty() { - return Err(DMError::new(location, format!("leftover: {:?} {}", from_utf8_or_latin1_borrowed(&input), leftover.len()))); + return Err(DMError::new( + location, + format!( + "leftover: {:?} {}", + from_utf8_or_latin1_borrowed(input), + leftover.len() + ), + )); } expr.simple_evaluate(location) } @@ -416,22 +460,31 @@ impl Expression { /// Evaluate this expression in the absence of any surrounding context. pub fn simple_evaluate(self, location: Location) -> Result { ConstantFolder { + context: None, tree: None, location, ty: NodeIndex::new(0), defines: None, - }.expr(self, None) + } + .expr(self, None) } } /// Evaluate an expression in the preprocessor, with `defined()` available. -pub fn preprocessor_evaluate(location: Location, expr: Expression, defines: &DefineMap) -> Result { +pub fn preprocessor_evaluate( + location: Location, + expr: Expression, + defines: &DefineMap, + context: Option<&Context>, +) -> Result { ConstantFolder { + context, tree: None, location, ty: NodeIndex::new(0), defines: Some(defines), - }.expr(expr, None) + } + .expr(expr, None) } /// Evaluate all the type-level variables in an object tree into constants. @@ -439,34 +492,28 @@ pub(crate) fn evaluate_all(context: &Context, tree: &mut ObjectTree) { for ty in tree.node_indices() { let keys: Vec = tree[ty].vars.keys().cloned().collect(); for key in keys { - if !tree[ty] - .get_var_declaration(&key, tree) - .map_or(true, |x| { - x.var_type.is_const_evaluable() && (x.var_type.flags.is_const() || ty != NodeIndex::new(0)) - }) - { - continue; // skip non-constant-evaluable vars + if !tree[ty].get_var_declaration(&key, tree).is_none_or(|x| { + x.var_type.is_const_evaluable() + && (x.var_type.flags.is_const() || ty != NodeIndex::new(0)) + }) { + continue; // skip non-constant-evaluable vars } - match constant_ident_lookup(tree, ty, &key, false) { + match constant_ident_lookup(tree, ty, &key, false, Some(context)) { Err(err) => context.register_error(err), - Ok(ConstLookup::Found(_, _)) => {} + Ok(ConstLookup::Found(_)) => {}, Ok(ConstLookup::Continue(_)) => { context.register_error(DMError::new( tree[ty].vars[&key].value.location, - format!( - "undefined var '{}' on type '{}'", - key, - tree[ty].path, - ), + format!("undefined var '{}' on type '{}'", key, tree[ty].path,), )); - } + }, } } } } enum ConstLookup { - Found(TreePath, Constant), + Found(Constant), Continue(Option), } @@ -475,14 +522,12 @@ fn constant_ident_lookup( ty: NodeIndex, ident: &str, must_be_const: bool, + context: Option<&Context>, ) -> Result { // try to read the currently-set value if we can and // substitute that in, otherwise try to evaluate it. let (location, type_hint, expr) = { - let decl = match tree[ty] - .get_var_declaration(ident, tree) - .cloned() - { + let decl = match tree[ty].get_var_declaration(ident, tree).cloned() { Some(decl) => decl, None => return Ok(ConstLookup::Continue(None)), // definitely doesn't exist }; @@ -492,52 +537,66 @@ fn constant_ident_lookup( match type_.vars.get_mut(ident) { None => return Ok(ConstLookup::Continue(parent)), Some(var) => match var.value.constant.clone() { - Some(constant) => return Ok(ConstLookup::Found(decl.var_type.type_path.clone(), constant)), + Some(constant) => { + return Ok(ConstLookup::Found( + /*decl.var_type.type_path,*/ constant, + )); + }, None => match var.value.expression.clone() { Some(expr) => { if var.value.being_evaluated { return Err(DMError::new( var.value.location, - format!("recursive constant reference: {}", ident), + format!("recursive constant reference: {ident}"), )); } else if !decl.var_type.is_const_evaluable() { return Err(DMError::new( var.value.location, - format!("non-const-evaluable variable: {}", ident), + format!("non-const-evaluable variable: {ident}"), )); } else if !decl.var_type.flags.is_const() && must_be_const { return Err(DMError::new( var.value.location, - format!("non-const variable: {}", ident), + format!("non-const variable: {ident}"), )); } var.value.being_evaluated = true; (var.value.location, decl.var_type.type_path, expr) - } + }, None => { let c = Constant::Null(Some(decl.var_type.type_path.clone())); var.value.constant = Some(c.clone()); - return Ok(ConstLookup::Found(decl.var_type.type_path, c)); - } + return Ok(ConstLookup::Found(/*decl.var_type.type_path,*/ c)); + }, }, }, } }; // evaluate full_value let value = ConstantFolder { + context, tree: Some(tree), defines: None, location, ty, - }.expr(expr, if type_hint.is_empty() { None } else { Some(&type_hint) })?; + } + .expr( + expr, + if type_hint.is_empty() { + None + } else { + Some(&type_hint) + }, + )?; // and store it into 'value', then return it let var = tree[ty].vars.get_mut(ident).unwrap(); var.value.constant = Some(value.clone()); var.value.being_evaluated = false; - Ok(ConstLookup::Found(type_hint, value)) + Ok(ConstLookup::Found(/*type_hint,*/ value)) } struct ConstantFolder<'a> { + context: Option<&'a Context>, tree: Option<&'a mut ObjectTree>, defines: Option<&'a DefineMap>, location: Location, @@ -551,14 +610,14 @@ impl<'a> HasLocation for ConstantFolder<'a> { } impl<'a> ConstantFolder<'a> { - fn expr(&mut self, expression: Expression, type_hint: Option<&TreePath>) -> Result { + fn expr( + &mut self, + expression: Expression, + type_hint: Option<&TreePath>, + ) -> Result { Ok(match expression { Expression::Base { term, follow } => { - let base_type_hint = if follow.is_empty() { - type_hint - } else { - None - }; + let base_type_hint = if follow.is_empty() { type_hint } else { None }; let mut term = self.term(term.elem, base_type_hint)?; for each in Vec::from(follow).into_iter() { term = self.follow(term, each.elem)?; @@ -570,11 +629,9 @@ impl<'a> ConstantFolder<'a> { let rhs = self.expr(*rhs, None)?; self.binary(lhs, rhs, op)? }, - Expression::TernaryOp { cond, if_, else_ } => { - match self.expr(*cond, None)?.to_bool() { - true => self.expr(*if_, type_hint)?, - false => self.expr(*else_, type_hint)?, - } + Expression::TernaryOp { cond, if_, else_ } => match self.expr(*cond, None)?.to_bool() { + true => self.expr(*if_, type_hint)?, + false => self.expr(*else_, type_hint)?, }, Expression::AssignOp { .. } => return Err(self.error("non-constant assignment")), }) @@ -591,7 +648,7 @@ impl<'a> ConstantFolder<'a> { } /// arguments or keyword arguments - fn arguments(&mut self, v: Box<[Expression]>) -> Result)]>, DMError> { + fn arguments(&mut self, v: Box<[Expression]>) -> Result, DMError> { let mut out = Vec::with_capacity(v.len()); for each in Vec::from(v).into_iter() { out.push(match each { @@ -627,13 +684,60 @@ impl<'a> ConstantFolder<'a> { full_path.push('/'); full_path.push_str(each); } - match self.tree.as_mut().and_then(|t| t.find(&full_path)).map(|t| t.index()) { + match self + .tree + .as_mut() + .and_then(|t| t.find(&full_path)) + .map(|t| t.index()) + { Some(idx) => self.recursive_lookup(idx, &field_name, true), - None => Err(self.error(format!("unknown typepath {}", full_path))), + None => Err(self.error(format!("unknown typepath {full_path}"))), } - } + }, (term, Follow::Unary(op)) => self.unary(term, op), - (term, follow) => Err(self.error(format!("non-constant expression follower: {} {:?}", term, follow))), + (term, Follow::StaticField(field)) => { + let Constant::Prefab(read_from) = term else { + return Err(self.error(format!("non typepath {term} used with ::"))); + }; + if !read_from.vars.is_empty() { + return Err(self.error(format!("non typepath {read_from} used with ::"))); + } + let Some(ref tree) = self.tree else { + return Err(self.error("no type tree available")); + }; + let Some(real_type) = + tree.find(FormatTreePath(&read_from.path).to_string().as_str()) + else { + return Err(self.error(format!( + "{} was not a valid type", + FormatTreePath(&read_from.path) + ))); + }; + self.recursive_lookup(real_type.index(), &field, false) + }, + (term, Follow::ProcReference(field)) => { + let Constant::Prefab(read_from) = term else { + return Err(self.error(format!("non typepath {term} used with ::"))); + }; + if !read_from.vars.is_empty() { + return Err(self.error(format!("non typepath {read_from} used with ::"))); + } + let Some(ref tree) = self.tree else { + return Err(self.error("no type tree available")); + }; + let Some(real_type) = + tree.find(FormatTreePath(&read_from.path).to_string().as_str()) + else { + return Err(self.error(format!( + "{} was not a valid type", + FormatTreePath(&read_from.path) + ))); + }; + self.proc_ref_lookup(real_type.index(), &field) + }, + (term, follow) => Err(self.error(format!( + "non-constant expression follower: {term} {follow:?}" + ))), } } @@ -647,11 +751,21 @@ impl<'a> ConstantFolder<'a> { (UnaryOp::Not, c) => Constant::from(!c.to_bool()), // float ops // unsupported - (op, term) => return Err(self.error(format!("non-constant unary operation: {}", op.around(&term)))), + (op, term) => { + return Err(self.error(format!( + "non-constant unary operation: {}", + op.around(&term) + ))) + }, }) } - fn binary(&mut self, mut lhs: Constant, mut rhs: Constant, op: BinaryOp) -> Result { + fn binary( + &mut self, + mut lhs: Constant, + mut rhs: Constant, + op: BinaryOp, + ) -> Result { use self::Constant::*; macro_rules! numeric { @@ -662,6 +776,7 @@ impl<'a> ConstantFolder<'a> { } } } + #[rustfmt::skip] numeric!(Add +); numeric!(Sub -); numeric!(Mul *); @@ -671,12 +786,22 @@ impl<'a> ConstantFolder<'a> { numeric!(LessEq <=); numeric!(Greater >); numeric!(GreaterEq >=); + match (op, lhs, rhs) { + (BinaryOp::FloatMod, Float(lhs), Float(rhs)) => { + return Ok(Constant::from(lhs - ((lhs / rhs).floor() * rhs))) + }, + (_, lhs_, rhs_) => { + lhs = lhs_; + rhs = rhs_; + }, + } + match (op, lhs, rhs) { (BinaryOp::Pow, Float(lhs), Float(rhs)) => return Ok(Constant::from(lhs.powf(rhs))), (_, lhs_, rhs_) => { lhs = lhs_; rhs = rhs_; - } + }, } macro_rules! integer { @@ -694,12 +819,14 @@ impl<'a> ConstantFolder<'a> { integer!(RShift >>); match (op, lhs, rhs) { - (BinaryOp::Add, String(lhs), String(rhs)) => Ok(String((std::string::String::from(lhs) + &rhs).into())), + (BinaryOp::Add, String(lhs), String(rhs)) => { + Ok(String((std::string::String::from(lhs) + &rhs).into())) + }, (BinaryOp::Eq, lhs, rhs) => Ok(Constant::from(lhs == rhs)), (BinaryOp::NotEq, lhs, rhs) => Ok(Constant::from(lhs != rhs)), (BinaryOp::And, lhs, rhs) => Ok(if lhs.to_bool() { rhs } else { lhs }), (BinaryOp::Or, lhs, rhs) => Ok(if lhs.to_bool() { lhs } else { rhs }), - (op, lhs, rhs) => Err(self.error(format!("non-constant {:?}: {} {} {}", op, lhs, op, rhs))), + (op, lhs, rhs) => Err(self.error(format!("non-constant {op:?}: {lhs} {op} {rhs}"))), } } @@ -728,6 +855,7 @@ impl<'a> ConstantFolder<'a> { "newlist" => Constant::Call(ConstFn::Newlist, self.arguments(args)?), "icon" => Constant::Call(ConstFn::Icon, self.arguments(args)?), "sound" => Constant::Call(ConstFn::Sound, self.arguments(args)?), + "filter" => Constant::Call(ConstFn::Filter, self.arguments(args)?), "file" => Constant::Call(ConstFn::File, self.arguments(args)?), "generator" => Constant::Call(ConstFn::Generator, self.arguments(args)?), // constant-evaluatable functions @@ -737,32 +865,147 @@ impl<'a> ConstantFolder<'a> { "arccos" => self.trig_op(args, f32::acos)?, "rgb" => Constant::String(self.rgb(args)?.into()), "defined" if self.defines.is_some() => { - let defines = self.defines.unwrap(); // annoying, but keeps the match clean + let defines = self.defines.unwrap(); // annoying, but keeps the match clean if args.len() != 1 { - return Err(self.error(format!("malformed defined() call, must have 1 argument and instead has {}", args.len()))); + return Err(self.error(format!( + "malformed defined() call, must have 1 argument and instead has {}", + args.len() + ))); } match args[0].as_term() { Some(Term::Ident(ref ident)) => Constant::from(defines.contains_key(ident)), - _ => return Err(self.error("malformed defined() call, argument given isn't an Ident.")), + _ => { + return Err(self + .error("malformed defined() call, argument given isn't an Ident.")) + }, } - } + }, + "nameof" => { + if args.len() != 1 { + return Err(self.error(format!( + "malformed nameof() call, must have 1 argument and instead has {}", + args.len() + ))); + } + match args[0].nameof() { + Some(name) => Constant::string(name), + None => { + return Err(self.error( + "malformed nameof() call, expression appears to have no name", + )) + }, + } + }, + "fexists" if self.defines.is_some() && self.context.is_some() => { + if args.len() != 1 { + return Err(self.error(format!( + "malformed fexists() call, must have 1 argument and instead has {}", + args.len() + ))); + } + match args[0].as_term() { + Some(Term::String(passed_path)) => { + let current_file_path = self.context.unwrap(/* is_some checked above */).file_path(self.location.file); + let Some(current_dir) = current_file_path.parent() else { + return Err(self.error(format!( + "fexists() file has no parent: {current_file_path:?}" + ))); + }; + current_dir.join(passed_path).exists().into() + }, + _ => { + return Err(self + .error("malformed fexists() call, argument given isn't a string.")) + }, + } + }, // other functions are no-goes - _ => return Err(self.error(format!("non-constant function call: {}", ident))), + _ => return Err(self.error(format!("non-constant function call: {ident}"))), }, Term::Prefab(prefab) => Constant::Prefab(Box::new(self.prefab(*prefab)?)), - Term::Ident(ident) => self.ident(ident, false)?, + Term::Ident(ident) => match ident.as_str() { + // We need to handle type and parent_type here + // They technically resolve to their respective values only in type defs when using :: + // But that's annoying so let's not + "type" => { + if let Some(obj_tree) = &self.tree { + let typeval = TypeRef::new(obj_tree, self.ty).get(); + let pop = Pop::from( + typeval + .path + .split('/') + .filter(|elem| !elem.is_empty()) + .map(|segment| segment.to_string()) + .collect::(), + ); + Constant::Prefab(Box::new(pop)) + } else { + return Err(self.error("no type context".to_owned())); + } + }, + "parent_type" => { + if let Some(obj_tree) = &self.tree { + let typeref = TypeRef::new(obj_tree, self.ty); + let Some(parent_type) = typeref.parent_type() else { + return Err(self.error(format!("no parent type for {typeref}"))); + }; + let pop = Pop::from( + parent_type + .path + .split('/') + .filter(|elem| !elem.is_empty()) + .map(|segment| segment.to_string()) + .collect::(), + ); + Constant::Prefab(Box::new(pop)) + } else { + return Err(self.error("no type context".to_owned())); + } + }, + _ => self.ident(ident, false)?, + }, Term::String(v) => Constant::String(v.into()), Term::Resource(v) => Constant::Resource(v.into()), Term::Int(v) => Constant::Float(v as f32), Term::Float(v) => Constant::from(v), Term::Expr(expr) => self.expr(*expr, type_hint)?, + Term::__TYPE__ => { + if let Some(obj_tree) = &self.tree { + let typeval = TypeRef::new(obj_tree, self.ty).get(); + let pop = Pop::from( + typeval + .path + .split('/') + .filter(|elem| !elem.is_empty()) + .map(|segment| segment.to_string()) + .collect::(), + ); + Constant::Prefab(Box::new(pop)) + } else { + return Err(self.error("No type context".to_owned())); + } + }, + Term::__IMPLIED_TYPE__ => { + if let Some(lhs_type) = type_hint { + Constant::Prefab(Box::new(Pop::from(lhs_type.clone()))) + } else { + return Err(self.error("No type hint".to_owned())); + } + }, _ => return Err(self.error("non-constant expression".to_owned())), }) } - fn trig_op(&mut self, args: Box<[Expression]>, op: fn(f32) -> f32) -> Result { + fn trig_op( + &mut self, + args: Box<[Expression]>, + op: fn(f32) -> f32, + ) -> Result { if args.len() != 1 { - Err(self.error(format!("trig function requires exactly 1 argument, instead found {}", args.len()))) + Err(self.error(format!( + "trig function requires exactly 1 argument, instead found {}", + args.len() + ))) } else if let Some(f) = self.expr(Vec::from(args).swap_remove(0), None)?.to_float() { Ok(Constant::Float(op(f))) } else { @@ -776,30 +1019,45 @@ impl<'a> ConstantFolder<'a> { // If the path is all slashes, it's absolute, and doesn't need to be // further resolved. if prefab.path.iter().all(|&(op, _)| op == PathOp::Slash) { - let path: TreePath = prefab.path.iter().map(|&(_, ref name)| name.to_owned()).collect(); - return Ok(Pop { path, vars }) + let path: TreePath = prefab + .path + .iter() + .map(|(_, name)| name.to_owned()) + .collect(); + return Ok(Pop { path, vars }); } // Otherwise, resolve it against our object tree, then stringify it. let tree = match self.tree.as_ref() { Some(tree) => tree, - None => return Err(self.error(format!( - "cannot resolve relative type path without an object tree: {}", - FormatTypePath(&prefab.path)))), + None => { + return Err(self.error(format!( + "cannot resolve relative type path without an object tree: {}", + FormatTypePath(&prefab.path) + ))) + }, }; let relative_to = TypeRef::new(tree, self.ty); let found = match relative_to.navigate_path(&prefab.path) { Some(found) => found, - None => return Err(self.error(format!("could not resolve {} relative to {}", - FormatTypePath(&prefab.path), relative_to))), + None => { + return Err(self.error(format!( + "could not resolve {} relative to {}", + FormatTypePath(&prefab.path), + relative_to + ))) + }, }; let path = found.to_path().into_boxed_slice(); Ok(Pop { path, vars }) } - fn vars(&mut self, input: Vec<(Ident2, Expression)>) -> Result, DMError> { + fn vars( + &mut self, + input: Vec<(Ident2, Expression)>, + ) -> Result, DMError> { // Visit the vars recursively. let mut vars = IndexMap::with_hasher(RandomState::default()); for (k, v) in input { @@ -814,22 +1072,53 @@ impl<'a> ConstantFolder<'a> { self.recursive_lookup(ty, &ident, must_be_const) } - fn recursive_lookup(&mut self, ty: NodeIndex, ident: &str, must_be_const: bool) -> Result { + fn recursive_lookup( + &mut self, + ty: NodeIndex, + ident: &str, + must_be_const: bool, + ) -> Result { let mut idx = Some(ty); while let Some(ty) = idx { let location = self.location; if self.tree.is_none() { - return Err(self.error(format!("cannot reference variable {:?} in this context", ident))); + return Err(self.error(format!( + "cannot reference variable {ident:?} in this context" + ))); } let tree = self.tree.as_mut().unwrap(); - match constant_ident_lookup(tree, ty, &ident, must_be_const) + match constant_ident_lookup(tree, ty, ident, must_be_const, self.context) .map_err(|e| e.with_location(location))? { - ConstLookup::Found(_, v) => return Ok(v), + ConstLookup::Found(v) => return Ok(v), ConstLookup::Continue(i) => idx = i, } } - Err(self.error(format!("unknown variable: {}", ident))) + Err(self.error(format!("unknown variable: {ident}"))) + } + + fn proc_ref_lookup(&mut self, ty: NodeIndex, name: &str) -> Result { + let tree = self.tree.as_mut().unwrap(); + let proc_type = TypeRef::new(tree, ty); + let Some(proc_ref) = proc_type.get_proc(name) else { + return Err(self.error(format!("unknown proc: {name}"))); + }; + // Gonna build the proc's path + let mut path_elements: Vec = proc_type + .get() + .path + .split('/') + .filter(|elem| !elem.is_empty()) + .map(|segment| segment.to_string()) + .collect(); + // Only tricky bit is adding on the type if required + if let Some(declaration) = proc_ref.get_declaration() { + path_elements.push(declaration.kind.name().to_string()); + } + path_elements.push(proc_ref.name().to_string()); + Ok(Constant::Prefab(Box::new(Pop::from(Box::from( + path_elements, + ))))) } fn rgb(&mut self, args: Box<[Expression]>) -> Result { @@ -855,7 +1144,10 @@ impl<'a> ConstantFolder<'a> { } if args.len() != 3 && args.len() != 4 && args.len() != 5 { - return Err(self.error(format!("malformed rgb() call, must have 3, 4, or 5 arguments and instead has {}", args.len()))); + return Err(self.error(format!( + "malformed rgb() call, must have 3, 4, or 5 arguments and instead has {}", + args.len() + ))); } let arguments = self.arguments(args)?; @@ -878,21 +1170,27 @@ impl<'a> ConstantFolder<'a> { "c" | "chroma" => color_args.c = true, "y" => color_args.y = true, "a" | "alpha" => color_args.a = kwarg_value.to_int(), - "space" => match kwarg_value.to_int() { // Do we have an actual colorspace specified? Set the values. + "space" => match kwarg_value.to_int() { + // Do we have an actual colorspace specified? Set the values. Some(0) => space = Some(ColorSpace::Rgb), Some(1) => space = Some(ColorSpace::Hsv), Some(2) => space = Some(ColorSpace::Hsl), Some(3) => space = Some(ColorSpace::Hcy), _ => { - return Err(self.error(format!("malformed rgb() call, bad color space: {}", kwarg_value))) - } - } + return Err(self.error(format!( + "malformed rgb() call, bad color space: {kwarg_value}" + ))) + }, + }, _ => { - return Err(self.error(format!("malformed rgb() call, bad kwarg passed: {}", kwarg))) - } + return Err(self + .error(format!("malformed rgb() call, bad kwarg passed: {kwarg}"))) + }, } } else { - return Err(self.error(format!("malformed rgb() call, kwarg is not string: {}", value))); + return Err(self.error(format!( + "malformed rgb() call, kwarg is not string: {value}" + ))); } } } @@ -900,27 +1198,29 @@ impl<'a> ConstantFolder<'a> { // Only set space if it wasn't set manually by the space arg let space = if let Some(space) = space { space - } else { - if color_args.r || color_args.g || color_args.b { - // TODO: Add hint here for useless r/g/b kwarg - ColorSpace::Rgb - } else if color_args.h { - if color_args.c && color_args.y { - ColorSpace::Hcy - } else if color_args.s { - if color_args.v { - ColorSpace::Hsv - } else if color_args.l { - ColorSpace::Hsl - } else { - return Err(self.error("malformed rgb() call, could not determine space: only h & s specified")); - } + } else if color_args.r || color_args.g || color_args.b { + // TODO: Add hint here for useless r/g/b kwarg + ColorSpace::Rgb + } else if color_args.h { + if color_args.c && color_args.y { + ColorSpace::Hcy + } else if color_args.s { + if color_args.v { + ColorSpace::Hsv + } else if color_args.l { + ColorSpace::Hsl } else { - return Err(self.error("malformed rgb() call, could not determine space: only h specified")); + return Err(self.error( + "malformed rgb() call, could not determine space: only h & s specified", + )); } } else { - ColorSpace::Rgb // Default + return Err( + self.error("malformed rgb() call, could not determine space: only h specified") + ); } + } else { + ColorSpace::Rgb // Default }; let mut value_vec: Vec = vec![]; @@ -966,20 +1266,28 @@ impl<'a> ConstantFolder<'a> { "a" | "alpha" => 0..=255, "space" => continue, // Don't range-check the value of the space _ => { - return Err(self.error(format!("malformed rgb() call, bad kwarg passed: {}", kwarg))) - } + return Err(self + .error(format!("malformed rgb() call, bad kwarg passed: {kwarg}"))) + }, }; } else { - return Err(self.error(format!("malformed rgb() call, kwarg is not string: {}", value))); + return Err(self.error(format!( + "malformed rgb() call, kwarg is not string: {value}" + ))); } } if let Some(i) = to_check.to_int() { if !range.contains(&i) { - return Err(self.error(format!("malformed rgb() call, {} is not within the valid range ({}..{})", i, range.start(), range.end())) + return Err(self + .error(format!( + "malformed rgb() call, {} is not within the valid range ({}..{})", + i, + range.start(), + range.end() + )) .set_severity(Severity::Warning) - .with_location(self.location) - ); + .with_location(self.location)); } let clamped = std::cmp::max(::std::cmp::min(i, *range.end()), *range.start()); value_vec.push(clamped.into()); @@ -993,19 +1301,34 @@ impl<'a> ConstantFolder<'a> { // Convert our color given a space to a rgb hexcode let color: Rgb = match space { ColorSpace::Rgb => Rgb::new(value_vec[0], value_vec[1], value_vec[2]), - ColorSpace::Hsv => Hsv::new(value_vec[0], value_vec[1] * 0.01, value_vec[2] * 0.01).into(), - ColorSpace::Hsl => Hsl::new(value_vec[0], value_vec[1] * 0.01, value_vec[2] * 0.01).into(), + ColorSpace::Hsv => { + Hsv::new(value_vec[0], value_vec[1] * 0.01, value_vec[2] * 0.01).into() + }, + ColorSpace::Hsl => { + Hsl::new(value_vec[0], value_vec[1] * 0.01, value_vec[2] * 0.01).into() + }, ColorSpace::Hcy => Lch::new(value_vec[2], value_vec[1], value_vec[0]).into(), }; // Extract the raw 4th alpha positional argument if it wasn't a kwarg - let alpha = color_args.a.or(value_vec.get(3).map(|&x| x as i32)); + let alpha = color_args.a.or_else(|| value_vec.get(3).map(|&x| x as i32)); // APPARENTLY the author thinks fractional rgb is a thing, hence the rounding if let Some(alpha) = alpha { - Ok(format!("#{:02x}{:02x}{:02x}{:02x}", color.r.round() as u8, color.g.round() as u8, color.b.round() as u8, alpha)) + Ok(format!( + "#{:02x}{:02x}{:02x}{:02x}", + color.r.round() as u8, + color.g.round() as u8, + color.b.round() as u8, + alpha + )) } else { - Ok(format!("#{:02x}{:02x}{:02x}", color.r.round() as u8, color.g.round() as u8, color.b.round() as u8)) + Ok(format!( + "#{:02x}{:02x}{:02x}", + color.r.round() as u8, + color.g.round() as u8, + color.b.round() as u8 + )) } } } diff --git a/crates/dreammaker/src/dmi.rs b/crates/dreammaker/src/dmi.rs index a031d8e3..e4b9d6ea 100644 --- a/crates/dreammaker/src/dmi.rs +++ b/crates/dreammaker/src/dmi.rs @@ -1,17 +1,47 @@ //! DMI metadata parsing and representation. +use foldhash::{HashMap, HashMapExt}; +use std::collections::BTreeMap; +use std::fmt::Display; use std::io; use std::path::Path; -use std::collections::BTreeMap; +use derivative::Derivative; use lodepng::Decoder; -const VERSION: &str = "4.0"; +const EXPECTED_VERSION_LINE: &str = "version = 4.0"; + +/// Index into the state name table +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub struct StateIndex(String, u32); + +impl Display for StateIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.1 == 0 { + write!(f, "{}", self.0) + } else { + write!(f, "{} ({})", self.0, self.1) + } + } +} + +impl From for StateIndex { + fn from(s: String) -> Self { + StateIndex(s, 0) + } +} + +impl From<&str> for StateIndex { + fn from(s: &str) -> Self { + StateIndex(s.to_owned(), 0) + } +} /// The two-dimensional facing subset of BYOND's direction type. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] pub enum Dir { North = 1, + #[default] South = 2, East = 4, West = 8, @@ -23,8 +53,22 @@ pub enum Dir { impl Dir { pub const CARDINALS: &'static [Dir] = &[Dir::North, Dir::South, Dir::East, Dir::West]; - pub const DIAGONALS: &'static [Dir] = &[Dir::Northeast, Dir::Northwest, Dir::Southeast, Dir::Southwest]; - pub const ALL: &'static [Dir] = &[Dir::North, Dir::South, Dir::East, Dir::West, Dir::Northeast, Dir::Northwest, Dir::Southeast, Dir::Southwest]; + pub const DIAGONALS: &'static [Dir] = &[ + Dir::Northeast, + Dir::Northwest, + Dir::Southeast, + Dir::Southwest, + ]; + pub const ALL: &'static [Dir] = &[ + Dir::North, + Dir::South, + Dir::East, + Dir::West, + Dir::Northeast, + Dir::Northwest, + Dir::Southeast, + Dir::Southwest, + ]; /// Attempt to build a direction from its integer representation. pub fn from_int(int: i32) -> Option { @@ -51,11 +95,7 @@ impl Dir { } pub fn is_diagonal(self) -> bool { - !matches!(self, - Dir::North - | Dir::South - | Dir::East - | Dir::West) + !matches!(self, Dir::North | Dir::South | Dir::East | Dir::West) } pub fn flip(self) -> Dir { @@ -164,14 +204,8 @@ impl Dir { } } -impl Default for Dir { - fn default() -> Self { - Dir::South - } -} - /// Embedded metadata describing a DMI spritesheet's layout. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub struct Metadata { /// The width of the icon in pixels. pub width: u32, @@ -180,20 +214,24 @@ pub struct Metadata { /// The list of states in the order they appear in the spritesheet. pub states: Vec, /// A lookup table from state name to its position in `states`. - pub state_names: BTreeMap, + pub state_names: BTreeMap, } /// The metadata belonging to a single icon state. -#[derive(Debug)] +#[derive(Derivative, Debug, Clone)] +#[derivative(PartialEq)] pub struct State { /// The state's name, corresponding to the `icon_state` var. pub name: String, /// Whether this is a movement state (shown during gliding). pub movement: bool, /// The number of frames in the spritesheet before this state's first frame. + #[derivative(PartialEq = "ignore")] pub offset: usize, /// 0 for infinite, 1+ for finite. pub loop_: u32, + /// The number of `State`s before this with the same name. + pub duplicate_index: u32, pub rewind: bool, pub dirs: Dirs, pub frames: Frames, @@ -208,7 +246,7 @@ pub enum Dirs { } /// How many frames of animation a state has, and their durations. -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Frames { /// Without an explicit setting, only one frame. One, @@ -223,21 +261,26 @@ impl Metadata { /// Read the bitmap and DMI metadata from a given file in a single pass. pub fn from_file(path: &Path) -> io::Result<(lodepng::Bitmap, Metadata)> { let path = &crate::fix_case(path); + Self::from_bytes(&std::fs::read(path)?) + } + + /// Read a u8 array (raw data of a file) as a DMI into a bitmap and metadata + pub fn from_bytes(data: &[u8]) -> io::Result<(lodepng::Bitmap, Metadata)> { let mut decoder = Decoder::new(); decoder.info_raw_mut().colortype = lodepng::ColorType::RGBA; decoder.info_raw_mut().set_bitdepth(8); decoder.remember_unknown_chunks(false); - let bitmap = match decoder.decode_file(path) { + let bitmap = match decoder.decode(data) { Ok(::lodepng::Image::RGBA(bitmap)) => bitmap, Ok(_) => return Err(io::Error::new(io::ErrorKind::InvalidData, "not RGBA")), Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), }; - let metadata = Metadata::from_decoder(bitmap.width as u32, bitmap.height as u32, &decoder); + let metadata = Metadata::from_decoder(bitmap.width as u32, bitmap.height as u32, &decoder)?; Ok((bitmap, metadata)) } - fn from_decoder(width: u32, height: u32, decoder: &Decoder) -> Metadata { + fn from_decoder(width: u32, height: u32, decoder: &Decoder) -> io::Result { for (key, value) in decoder.info_png().text_keys() { if key == b"Description" { if let Ok(value) = std::str::from_utf8(value) { @@ -246,30 +289,31 @@ impl Metadata { break; } } - Metadata { + Ok(Metadata { width, height, states: Default::default(), state_names: Default::default(), - } + }) } /// Parse metadata from a `Description` string. #[inline] - pub fn meta_from_str(data: &str) -> Metadata { + pub fn meta_from_str(data: &str) -> io::Result { parse_metadata(data) } - pub fn rect_of(&self, bitmap_width: u32, icon_state: &str, dir: Dir, frame: u32) -> Option<(u32, u32, u32, u32)> { + pub fn rect_of( + &self, + bitmap_width: u32, + icon_state: &StateIndex, + dir: Dir, + frame: u32, + ) -> Option<(u32, u32, u32, u32)> { if self.states.is_empty() { return Some((0, 0, self.width, self.height)); } - let state_index = match self.state_names.get(icon_state) { - Some(&i) => i, - None if icon_state == "" => 0, - None => return None, - }; - let state = &self.states[state_index]; + let state = self.get_icon_state(icon_state)?; let icon_index = state.index_of_frame(dir, frame); let icon_count = bitmap_width / self.width; @@ -281,11 +325,27 @@ impl Metadata { self.height, )) } + + pub fn get_icon_state(&self, icon_state: &StateIndex) -> Option<&State> { + let state_index = match self.state_names.get(icon_state) { + Some(&i) => i, + None => return None, + }; + Some(&self.states[state_index]) + } } impl State { + pub fn is_animated(&self) -> bool { + match self.frames { + Frames::One | Frames::Count(1) => false, + Frames::Count(_) => true, + Frames::Delays(_) => true, + } + } + pub fn num_sprites(&self) -> usize { - self.dirs.len() * self.frames.len() + self.dirs.count() * self.frames.count() } pub fn index_of_dir(&self, dir: Dir) -> u32 { @@ -306,12 +366,16 @@ impl State { #[inline] pub fn index_of_frame(&self, dir: Dir, frame: u32) -> u32 { - self.index_of_dir(dir) + frame * self.dirs.len() as u32 + self.index_of_dir(dir) + frame * self.dirs.count() as u32 + } + + pub fn get_state_name_index(&self) -> StateIndex { + StateIndex(self.name.clone(), self.duplicate_index) } } impl Dirs { - pub fn len(self) -> usize { + pub fn count(self) -> usize { match self { Dirs::One => 1, Dirs::Four => 4, @@ -321,7 +385,7 @@ impl Dirs { } impl Frames { - pub fn len(&self) -> usize { + pub fn count(&self) -> usize { match *self { Frames::One => 1, Frames::Count(n) => n, @@ -341,7 +405,7 @@ impl Frames { // ---------------------------------------------------------------------------- // Metadata parser -fn parse_metadata(data: &str) -> Metadata { +fn parse_metadata(data: &str) -> io::Result { let mut metadata = Metadata { width: 32, height: 32, @@ -349,47 +413,65 @@ fn parse_metadata(data: &str) -> Metadata { state_names: BTreeMap::new(), }; if data.is_empty() { - return metadata; + return Ok(metadata); } let mut lines = data.lines(); - assert_eq!(lines.next().unwrap(), "# BEGIN DMI"); - assert_eq!(lines.next().unwrap(), &format!("version = {}", VERSION)); + let header = (lines.next(), lines.next()); + let expected_header = (Some("# BEGIN DMI"), Some(EXPECTED_VERSION_LINE)); + if header != expected_header { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Wrong dmi metadata header. Expected {expected_header:?}, got {header:?}"), + )); + } let mut state: Option = None; let mut frames_so_far = 0; + let mut duplicate_map: HashMap = HashMap::new(); + for line in lines { if line.starts_with("# END DMI") { break; } - let mut split = line.trim().splitn(2, " = "); - let key = split.next().unwrap(); - let value = split.next().unwrap(); + let (key, value) = line.trim().split_once(" = ").unwrap(); match key { "width" => metadata.width = value.parse().unwrap(), "height" => metadata.height = value.parse().unwrap(), "state" => { if let Some(state) = state.take() { - frames_so_far += state.frames.len() * state.dirs.len(); + frames_so_far += state.frames.count() * state.dirs.count(); metadata.states.push(state); } let unquoted = value[1..value.len() - 1].to_owned(); // TODO: unquote assert!(!unquoted.contains('\\') && !unquoted.contains('"')); - if !metadata.state_names.contains_key(&unquoted) { - metadata.state_names.insert(unquoted.clone(), metadata.states.len()); - } - state = Some(State { + let count = duplicate_map.entry(unquoted.clone()).or_insert(0); + + let new_state = State { offset: frames_so_far, name: unquoted, loop_: 0, + duplicate_index: *count, rewind: false, movement: false, dirs: Dirs::One, frames: Frames::One, - }); - } + }; + + let key = new_state.get_state_name_index(); + + if let std::collections::btree_map::Entry::Vacant(e) = + metadata.state_names.entry(key) + { + e.insert(metadata.states.len()); + } + + state = Some(new_state); + + *count += 1; + }, "dirs" => { let state = state.as_mut().unwrap(); let n: u8 = value.parse().unwrap(); @@ -399,7 +481,7 @@ fn parse_metadata(data: &str) -> Metadata { 8 => Dirs::Eight, _ => panic!(), }; - } + }, "frames" => { let state = state.as_mut().unwrap(); match state.frames { @@ -407,31 +489,125 @@ fn parse_metadata(data: &str) -> Metadata { _ => panic!(), } state.frames = Frames::Count(value.parse().unwrap()); - } + }, "delay" => { let state = state.as_mut().unwrap(); - let mut vector: Vec = value.split(',').map(str::parse).collect::, _>>().unwrap(); + let mut vector: Vec = value + .split(',') + .map(str::parse) + .collect::, _>>() + .unwrap(); match state.frames { - Frames::One => if vector.iter().all(|&n| n == 1.) { - state.frames = Frames::Count(vector.len()); - } else { - state.frames = Frames::Delays(vector); + Frames::One => { + if vector.iter().all(|&n| n == 1.) { + state.frames = Frames::Count(vector.len()); + } else { + state.frames = Frames::Delays(vector); + } }, - Frames::Count(n) => if !vector.iter().all(|&n| n == 1.) { + Frames::Count(n) => { vector.truncate(n); - state.frames = Frames::Delays(vector); + if !vector.iter().all(|&n| n == 1.) { + state.frames = Frames::Delays(vector); + } }, Frames::Delays(_) => panic!(), } - } + }, "loop" => state.as_mut().unwrap().loop_ = value.parse().unwrap(), "rewind" => state.as_mut().unwrap().rewind = value.parse::().unwrap() != 0, - "hotspot" => { /* TODO */ } + "hotspot" => { /* TODO */ }, "movement" => state.as_mut().unwrap().movement = value.parse::().unwrap() != 0, _ => panic!(), } } metadata.states.extend(state); - metadata + Ok(metadata) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn duplicate_states() { + let description = r#" +# BEGIN DMI +version = 4.0 + width = 32 + height = 32 +state = "duplicate" + dirs = 1 + frames = 1 +state = "duplicate" + dirs = 1 + frames = 1 +state = "duplicate" + dirs = 1 + frames = 1 +# END DMI +"# + .trim(); + + let metadata = parse_metadata(description).expect("Metadata is valid"); + assert_eq!(metadata.state_names.len(), 3); + assert_eq!( + metadata.state_names, + BTreeMap::from([ + (StateIndex("duplicate".to_owned(), 0), 0), + (StateIndex("duplicate".to_owned(), 1), 1), + (StateIndex("duplicate".to_owned(), 2), 2) + ]) + ); + assert_eq!(metadata.states.len(), 3); + + for (no, state) in metadata.states.iter().enumerate() { + if no == 0 { + assert_eq!(state.duplicate_index, 0) + } else { + assert_eq!(state.duplicate_index, no as u32); + } + + // Note: using `no` here only works by virtue of the test data being only composed of duplicates + assert_eq!( + no, + *metadata + .state_names + .get(&state.get_state_name_index()) + .unwrap() + ) + } + } + + #[test] + /// Sometimes, Dream Maker just doesn't get rid of extra delay + /// information when a state has the number of frames edited. + /// + /// This means we need to truncate our delay list to the number of frames specified by the frames key. + /// + /// This always worked fine- however, we also simplify `delays = 1,1,...` to `Frames::Count(delays.len())`. + /// + /// The bug in our code was that we checked if our `delays = 1,1,...` *before* truncating the array + /// in the truncation case, so we would output `Frames::Delays([1,1])` for this metadata. + fn delay_overflow_edge_case() { + let description = r#" +# BEGIN DMI +version = 4.0 + width = 32 + height = 32 +state = "one" + dirs = 1 + frames = 2 + delay = 1,1,0.5,0.5 +# END DMI +"# + .trim(); + + let metadata = parse_metadata(description).expect("Metadata is valid"); + let state = metadata + .get_icon_state(&StateIndex("one".to_owned(), 0)) + .expect("Only one state, named one, should be found"); + assert_eq!(state.frames, Frames::Count(2)); + } } diff --git a/crates/dreammaker/src/docs.rs b/crates/dreammaker/src/docs.rs index 0fe0b9bc..3c4dc144 100644 --- a/crates/dreammaker/src/docs.rs +++ b/crates/dreammaker/src/docs.rs @@ -2,8 +2,11 @@ use std::fmt; +use get_size::GetSize; +use get_size_derive::GetSize; + /// A collection of documentation comments targeting the same item. -#[derive(Default, Clone, Debug, PartialEq)] +#[derive(Default, Clone, Debug, PartialEq, GetSize)] pub struct DocCollection { elems: Vec, pub builtin_docs: BuiltinDocs, @@ -16,8 +19,8 @@ impl DocCollection { } /// Combine another collection into this one. - pub fn extend(&mut self, collection: DocCollection) { - self.elems.extend(collection.elems); + pub fn extend(&mut self, collection: impl IntoIterator) { + self.elems.extend(collection); } /// Check whether this collection is empty. @@ -59,8 +62,18 @@ impl DocCollection { } } +impl IntoIterator for DocCollection { + type Item = DocComment; + + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.elems.into_iter() + } +} + /// A documentation comment. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, GetSize)] pub struct DocComment { pub kind: CommentKind, pub target: DocTarget, @@ -81,6 +94,16 @@ impl DocComment { fn is_empty(&self) -> bool { is_empty(&self.text, self.kind.ignore_char()) } + + /// Return the 3-character sequence that started this comment. + pub fn describe_type(&self) -> &'static str { + match (self.kind, self.target) { + (CommentKind::Block, DocTarget::FollowingItem) => "/**", + (CommentKind::Block, DocTarget::EnclosingItem) => "/*!", + (CommentKind::Line, DocTarget::FollowingItem) => "///", + (CommentKind::Line, DocTarget::EnclosingItem) => "//!", + } + } } impl fmt::Display for DocComment { @@ -88,14 +111,14 @@ impl fmt::Display for DocComment { match (self.kind, self.target) { (CommentKind::Block, DocTarget::FollowingItem) => write!(f, "/**{}*/", self.text), (CommentKind::Block, DocTarget::EnclosingItem) => write!(f, "/*!{}*/", self.text), - (CommentKind::Line, DocTarget::FollowingItem) => write!(f, "///{}", self.text), - (CommentKind::Line, DocTarget::EnclosingItem) => write!(f, "//!{}", self.text), + (CommentKind::Line, DocTarget::FollowingItem) => write!(f, "///{}", self.text), + (CommentKind::Line, DocTarget::EnclosingItem) => write!(f, "//!{}", self.text), } } } /// The possible documentation comment kinds. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, GetSize)] pub enum CommentKind { /// A block `/** */` comment. Block, @@ -126,9 +149,10 @@ fn simplify(out: &mut String, text: &str, ignore_char: char) -> bool { continue; } - let this_prefix = &line[..line.len() - line - .trim_start_matches(|c: char| c.is_whitespace() || c == ignore_char) - .len()]; + let this_prefix = &line[..line.len() + - line + .trim_start_matches(|c: char| c.is_whitespace() || c == ignore_char) + .len()]; match prefix { None => prefix = Some(this_prefix), Some(ref mut prefix) => { @@ -137,17 +161,21 @@ fn simplify(out: &mut String, text: &str, ignore_char: char) -> bool { loop { no_match = chars.as_str(); match chars.next() { - Some(ch) => if Some(ch) != this_chars.next() { - break; + Some(ch) => { + if Some(ch) != this_chars.next() { + break; + } }, None => break, } } *prefix = &prefix[..prefix.len() - no_match.len()]; - } + }, } - let this_suffix = &line[line.trim_end_matches(|c: char| c.is_whitespace() || c == ignore_char).len()..]; + let this_suffix = &line[line + .trim_end_matches(|c: char| c.is_whitespace() || c == ignore_char) + .len()..]; match suffix { None => suffix = Some(this_suffix), Some(ref mut suffix) => { @@ -156,14 +184,16 @@ fn simplify(out: &mut String, text: &str, ignore_char: char) -> bool { loop { no_match = chars.as_str(); match chars.next_back() { - Some(ch) => if Some(ch) != this_chars.next_back() { - break; + Some(ch) => { + if Some(ch) != this_chars.next_back() { + break; + } }, None => break, } } *suffix = &suffix[no_match.len()..]; - } + }, } } @@ -197,7 +227,7 @@ fn is_empty(text: &str, ignore_char: char) -> bool { } /// The possible items that a documentation comment may target. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, GetSize)] pub enum DocTarget { /// Starting with `*` or `/`, referring to the following item. FollowingItem, @@ -206,15 +236,10 @@ pub enum DocTarget { } /// Information about where builtin docs can be found. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, GetSize, Default)] pub enum BuiltinDocs { + #[default] None, /// A DM reference hash such as "/DM/vars". ReferenceHash(&'static str), } - -impl Default for BuiltinDocs { - fn default() -> Self { - BuiltinDocs::None - } -} diff --git a/crates/dreammaker/src/error.rs b/crates/dreammaker/src/error.rs index 8620a298..cb45a278 100644 --- a/crates/dreammaker/src/error.rs +++ b/crates/dreammaker/src/error.rs @@ -1,13 +1,13 @@ //! Error, warning, and other diagnostics handling. -use std::{fmt, error, io}; -use std::path::{PathBuf, Path}; -use std::cell::{RefCell, Ref, RefMut}; -use std::collections::HashMap; +use foldhash::HashMap; +use std::cell::{Ref, RefCell, RefMut}; +use std::path::{Path, PathBuf}; +use std::{error, fmt, io}; -use ahash::RandomState; - -use termcolor::{ColorSpec, Color}; +use get_size::GetSize; +use get_size_derive::GetSize; +use termcolor::{Color, ColorSpec}; use crate::config::Config; @@ -15,6 +15,8 @@ use crate::config::Config; #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct FileId(u16); +impl GetSize for FileId {} + const FILEID_BUILTINS: FileId = FileId(0x0000); const FILEID_MIN: FileId = FileId(0x0001); const FILEID_MAX: FileId = FileId(0xfffe); @@ -32,7 +34,7 @@ pub struct FileList { /// The list of loaded files. files: RefCell>, /// Reverse mapping from paths to file numbers. - reverse_files: RefCell>, + reverse_files: RefCell>, } /// A diagnostics context, tracking loaded files and any observed errors. @@ -42,7 +44,7 @@ pub struct Context { /// A list of errors, warnings, and other diagnostics generated. errors: RefCell>, /// Warning config - config: RefCell, + config: Config, print_severity: Option, io_time: std::cell::Cell, @@ -71,16 +73,16 @@ impl FileList { } /// Look up a file path by its index returned from `register_file`. - pub fn get_path(&self, file: FileId) -> PathBuf { + pub fn get_path(&self, file: FileId) -> Ref<'_, Path> { + let files = self.files.borrow(); if file == FILEID_BUILTINS { - return "(builtins)".into(); + return Ref::map(files, |_| Path::new("(builtins)")); } let idx = (file.0 - FILEID_MIN.0) as usize; - let files = self.files.borrow(); if idx > files.len() { - "(unknown)".into() + Ref::map(files, |_| Path::new("(unknown)")) } else { - files[idx].to_owned() + Ref::map(files, |files| files[idx].as_path()) } } @@ -106,7 +108,7 @@ impl Context { } /// Look up a file path by its index returned from `register_file`. - pub fn file_path(&self, file: FileId) -> PathBuf { + pub fn file_path(&self, file: FileId) -> Ref<'_, Path> { self.files.get_path(file) } @@ -122,28 +124,31 @@ impl Context { // ------------------------------------------------------------------------ // Configuration - pub fn force_config(&self, toml: &Path) { + pub fn force_config(&mut self, toml: &Path) { match Config::read_toml(toml) { - Ok(config) => *self.config.borrow_mut() = config, + Ok(config) => self.config = config, Err(io_error) => { let file = self.register_file(toml); let (line, column) = io_error.line_col().unwrap_or((1, 1)); - DMError::new(Location { file, line, column }, "Error reading configuration file") - .with_boxed_cause(io_error.into_boxed_error()) - .register(self); - } + DMError::new( + Location { file, line, column }, + "Error reading configuration file", + ) + .with_boxed_cause(io_error.into_boxed_error()) + .register(self); + }, } } - pub fn autodetect_config(&self, dme: &Path) { + pub fn autodetect_config(&mut self, dme: &Path) { let toml = dme.parent().unwrap().join("SpacemanDMM.toml"); if toml.exists() { self.force_config(&toml); } } - pub fn config(&self) -> Ref { - self.config.borrow() + pub fn config(&self) -> &Config { + &self.config } /// Set a severity at and above which errors will be printed immediately. @@ -171,12 +176,12 @@ impl Context { /// Push an error or other diagnostic to the context. pub fn register_error(&self, error: DMError) { - guard!(let Some(error) = self.config.borrow().set_configured_severity(error) else { - return // errortype is disabled - }); + let Some(error) = self.config.set_configured_severity(error) else { + return; // errortype is disabled + }; // ignore errors with severity above configured level - if !self.config.borrow().registerable_error(&error) { - return + if !self.config.registerable_error(&error) { + return; } if let Some(print_severity) = self.print_severity { if error.severity() <= print_severity { @@ -189,18 +194,22 @@ impl Context { } /// Access the list of diagnostics generated so far. - pub fn errors(&self) -> Ref<[DMError]> { + pub fn errors(&self) -> Ref<'_, [DMError]> { Ref::map(self.errors.borrow(), |x| &**x) } /// Mutably access the diagnostics list. Dangerous. #[doc(hidden)] - pub fn errors_mut(&self) -> RefMut> { + pub fn errors_mut(&self) -> RefMut<'_, Vec> { self.errors.borrow_mut() } /// Pretty-print a `DMError` to the given output. - pub fn pretty_print_error(&self, w: &mut W, error: &DMError) -> io::Result<()> { + pub fn pretty_print_error( + &self, + w: &mut W, + error: &DMError, + ) -> io::Result<()> { writeln!( w, "{}, line {}, column {}:", @@ -216,14 +225,12 @@ impl Context { for note in error.notes().iter() { if note.location == error.location { - writeln!(w, "- {}", note.description, )?; + writeln!(w, "- {}", note.description,)?; } else if note.location.file == error.location.file { writeln!( w, "- {}:{}: {}", - note.location.line, - note.location.column, - note.description, + note.location.line, note.location.column, note.description, )?; } else { writeln!( @@ -239,7 +246,11 @@ impl Context { writeln!(w) } - pub fn pretty_print_error_nocolor(&self, w: &mut W, error: &DMError) -> io::Result<()> { + pub fn pretty_print_error_nocolor( + &self, + w: &mut W, + error: &DMError, + ) -> io::Result<()> { self.pretty_print_error(&mut termcolor::NoColor::new(w), error) } @@ -253,7 +264,8 @@ impl Context { let mut printed = false; for err in errors.iter() { if err.severity <= min_severity { - self.pretty_print_error(stderr, &err).expect("error writing to stderr"); + self.pretty_print_error(stderr, err) + .expect("error writing to stderr"); printed = true; } } @@ -273,7 +285,7 @@ impl Context { // Location handling /// File, line, and column information for an error. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, GetSize)] pub struct Location { /// The index into the file table. pub file: FileId, @@ -285,7 +297,11 @@ pub struct Location { impl Location { pub fn builtins() -> Location { - Location { file: FILEID_BUILTINS, line: 1, column: 1 } + Location { + file: FILEID_BUILTINS, + line: 1, + column: 1, + } } /// Pack this Location for use in `u64`-keyed structures. @@ -300,6 +316,10 @@ impl Location { } else if self.line != 0 { self.column = !0; self.line -= 1; + } else if self.file == FILEID_BAD { + // This file ID generally comes from using Location::default(). + // In that case hopefully it's a test or something, so just let it + // stay 0:0. } else if self.file.0 != 0 { self.column = !0; self.line = !0; @@ -336,8 +356,9 @@ pub(crate) trait HasLocation { // Error handling /// The possible diagnostic severities available. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)] pub enum Severity { + #[default] Error = 1, Warning = 2, Info = 3, @@ -348,21 +369,21 @@ impl Severity { fn style(self) -> ColorSpec { let mut spec = ColorSpec::new(); match self { - Severity::Error => { spec.set_fg(Some(Color::Red)); } - Severity::Warning => { spec.set_fg(Some(Color::Yellow)); } - Severity::Info => { spec.set_fg(Some(Color::White)).set_intense(true); } + Severity::Error => { + spec.set_fg(Some(Color::Red)); + }, + Severity::Warning => { + spec.set_fg(Some(Color::Yellow)); + }, + Severity::Info => { + spec.set_fg(Some(Color::White)).set_intense(true); + }, Severity::Hint => {}, } spec } } -impl Default for Severity { - fn default() -> Severity { - Severity::Error - } -} - impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -375,8 +396,9 @@ impl fmt::Display for Severity { } /// A component which generated a diagnostic, when separation is desired. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub enum Component { + #[default] Unspecified, DreamChecker, } @@ -390,12 +412,6 @@ impl Component { } } -impl Default for Component { - fn default() -> Component { - Component::Unspecified - } -} - impl fmt::Display for Component { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.name() { @@ -519,7 +535,24 @@ impl DMError { impl fmt::Display for DMError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}:{}:{}", self.location.line, self.location.column, self.description) + // Like `pretty_print_error` above, but without filename information. + write!( + f, + "{}:{}: {}: {}", + self.location.line, self.location.column, self.severity, self.description + )?; + for note in self.notes.iter() { + if note.location == self.location { + write!(f, "\n- {}", note.description,)?; + } else { + write!( + f, + "\n- {}:{}: {}", + note.location.line, note.location.column, note.description, + )?; + } + } + Ok(()) } } @@ -541,7 +574,7 @@ impl Clone for DMError { component: self.component, description: self.description.clone(), notes: self.notes.clone(), - cause: None, // not trivially cloneable + cause: None, // not trivially cloneable errortype: self.errortype, } } diff --git a/crates/dreammaker/src/indents.rs b/crates/dreammaker/src/indents.rs index 06261ae5..8487b4ba 100644 --- a/crates/dreammaker/src/indents.rs +++ b/crates/dreammaker/src/indents.rs @@ -1,8 +1,8 @@ //! The indentation processor. use std::collections::VecDeque; -use crate::{Location, Context, DMError}; -use crate::lexer::{LocatedToken, Token, Punctuation}; +use crate::lexer::{LocatedToken, Punctuation, Token}; +use crate::{Context, DMError, Location}; /// Eliminates blank lines, parses and validates indentation, braces, and semicolons. /// @@ -23,10 +23,14 @@ pub struct IndentProcessor<'ctx, I> { eof_yielded: bool, } -impl<'ctx, I> IndentProcessor<'ctx, I> where - I: Iterator +impl<'ctx, I> IndentProcessor<'ctx, I> +where + I: Iterator, { - pub fn new>(context: &'ctx Context, inner: J) -> Self { + pub fn new>( + context: &'ctx Context, + inner: J, + ) -> Self { IndentProcessor { context, inner: inner.into_iter(), @@ -47,12 +51,16 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where #[inline] fn push(&mut self, tok: Token) { - self.output.push_back(LocatedToken::new(self.last_input_loc, tok)); + self.output + .push_back(LocatedToken::new(self.last_input_loc, tok)); } #[inline] fn push_eol(&mut self, tok: Token) { - self.output.push_back(LocatedToken::new(self.eol_location.unwrap_or(self.last_input_loc), tok)); + self.output.push_back(LocatedToken::new( + self.eol_location.unwrap_or(self.last_input_loc), + tok, + )); } #[inline] @@ -72,15 +80,14 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where self.eol_location = Some(self.last_input_loc); } return; - } - Token::Punct(Punctuation::Tab) | - Token::Punct(Punctuation::Space) => { + }, + Token::Punct(Punctuation::Tab) | Token::Punct(Punctuation::Space) => { if let Some(spaces) = self.current_spaces.as_mut() { *spaces += 1; } return; - } - _ => {} + }, + _ => {}, } // handle pre-existing braces @@ -88,8 +95,8 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where Token::Punct(Punctuation::LBrace) => self.current_spaces = None, Token::Punct(Punctuation::RBrace) => { self.current_spaces = None; - } - _ => {} + }, + _ => {}, } // handle indentation @@ -105,7 +112,7 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where new_indents = 1; self.current = Some((spaces, 1)); } - } + }, Some((spaces_per_indent, indents_)) => { indents = indents_; if spaces == 0 { @@ -116,15 +123,18 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where // Register the error, but cross our fingers and // hope that truncating division will approximate // a sane situation. - DMError::new(self.last_input_loc, format!( - "inconsistent indentation: {} % {} != 0", - spaces, spaces_per_indent, - )).register(self.context) + DMError::new( + self.last_input_loc, + format!( + "inconsistent indentation: {spaces} % {spaces_per_indent} != 0", + ), + ) + .register(self.context) } new_indents = spaces / spaces_per_indent; self.current = Some((spaces_per_indent, new_indents)); } - } + }, } if indents + 1 == new_indents { @@ -132,20 +142,24 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where self.push_eol(Token::Punct(Punctuation::LBrace)); } else if indents < new_indents { // multiple indent is an error, register it but let it work - DMError::new(self.last_input_loc, format!( - "inconsistent multiple indentation: {} > 1", - new_indents - indents, - )).register(self.context); + DMError::new( + self.last_input_loc, + format!( + "inconsistent multiple indentation: {} > 1", + new_indents - indents, + ), + ) + .register(self.context); for _ in indents..new_indents { self.push_eol(Token::Punct(Punctuation::LBrace)); } } else if indents == new_indents + 1 { // single unindent - self.push(Token::Punct(Punctuation::RBrace)); + self.push_eol(Token::Punct(Punctuation::RBrace)); } else if indents > new_indents { // multiple unindent for _ in new_indents..indents { - self.push(Token::Punct(Punctuation::RBrace)); + self.push_eol(Token::Punct(Punctuation::RBrace)); } } else { // same indent as before @@ -160,24 +174,25 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where None => Some((1, 1)), Some((x, y)) => Some((x, y + 1)), }; - } + }, Token::Punct(Punctuation::RBrace) => { self.current = match self.current { None => { - DMError::new(self.last_input_loc, "unmatched right brace").register(self.context); + DMError::new(self.last_input_loc, "unmatched right brace") + .register(self.context); None - } + }, Some((_, 1)) => None, Some((x, y)) => Some((x, y - 1)), }; - } + }, Token::Punct(Punctuation::LParen) => { self.parentheses += 1; - } + }, Token::Punct(Punctuation::RParen) => { self.parentheses = self.parentheses.saturating_sub(1); - } - _ => {} + }, + _ => {}, } self.eol_location = None; @@ -185,8 +200,9 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where } } -impl<'ctx, I> Iterator for IndentProcessor<'ctx, I> where - I: Iterator +impl<'ctx, I> Iterator for IndentProcessor<'ctx, I> +where + I: Iterator, { type Item = LocatedToken; diff --git a/crates/dreammaker/src/lexer.rs b/crates/dreammaker/src/lexer.rs index 9e708b03..961830b0 100644 --- a/crates/dreammaker/src/lexer.rs +++ b/crates/dreammaker/src/lexer.rs @@ -1,12 +1,12 @@ //! The lexer/tokenizer. +use std::borrow::Cow; +use std::fmt; use std::io::Read; use std::str::FromStr; -use std::fmt; -use std::borrow::Cow; -use super::{DMError, Location, HasLocation, FileId, Context, Severity}; -use super::docs::*; use super::ast::Ident; +use super::docs::*; +use super::{Context, DMError, FileId, HasLocation, Location, Severity}; macro_rules! table { ( @@ -74,6 +74,8 @@ table! { "#", Hash; "##", TokenPaste; "%", Mod; + "%%", FloatMod; + "%%=", FloatModAssign; "%=", ModAssign; "&", BitAnd; "&&", And; @@ -100,12 +102,14 @@ table! { "//", LineComment; "/=", DivAssign; ":", Colon -> CloseColon; + "::", Scope; ":=", AssignInto; ";", Semicolon; "<", Less; "<<", LShift; "<<=", LShiftAssign; "<=", LessEq; + "<=>", LessOrGreater; "<>", LessGreater; "=", Assign; "==", Eq; @@ -144,23 +148,25 @@ impl fmt::Display for Punctuation { /// This lookup table is used to keep `read_punct`, called for essentially each /// character in the input, blazing fast. The code to generate it is contained /// in the following test. +#[rustfmt::skip] static SPEEDY_TABLE: [(usize, usize); 127] = [ (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 1), (1, 2), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), - (2, 3), (3, 5), (5, 6), (6, 8), (0, 0), (8, 10), (10, 14), (14, 15), - (15, 16), (16, 17), (17, 20), (20, 23), (23, 24), (24, 27), (27, 30), (30, 34), + (2, 3), (3, 5), (5, 6), (6, 8), (0, 0), (8, 12), (12, 16), (16, 17), + (17, 18), (18, 19), (19, 22), (22, 25), (25, 26), (26, 29), (29, 32), (32, 36), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), - (0, 0), (0, 0), (34, 36), (36, 37), (37, 42), (42, 44), (44, 48), (48, 52), + (0, 0), (0, 0), (36, 39), (39, 40), (40, 46), (46, 48), (48, 52), (52, 56), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), - (0, 0), (0, 0), (0, 0), (52, 53), (0, 0), (53, 54), (54, 56), (0, 0), + (0, 0), (0, 0), (0, 0), (56, 57), (0, 0), (57, 58), (58, 60), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), - (0, 0), (0, 0), (0, 0), (56, 58), (58, 62), (62, 63), (63, 66)]; + (0, 0), (0, 0), (0, 0), (60, 62), (62, 66), (66, 67), (67, 70) +]; #[test] fn make_speedy_table() { @@ -172,8 +178,7 @@ fn make_speedy_table() { for each in everything.iter() { assert!( each.len() == 1 || everything.contains(&&each[..each.len() - 1]), - "no prefix: {}", - each + "no prefix: {each}" ); } @@ -185,7 +190,10 @@ fn make_speedy_table() { } if let Some(prev) = prev { - assert!(each > prev, "out-of-order: {:?} is not greater than {:?}", each, prev); + assert!( + each > prev, + "out-of-order: {each:?} is not greater than {prev:?}" + ); } prev = Some(each); @@ -198,12 +206,12 @@ fn make_speedy_table() { table[b].1 = i + 1; } else { assert!(i >= table[b].0); - assert_eq!(i, table[b].1, "{}", each); + assert_eq!(i, table[b].1, "{each}"); table[b].1 = i + 1; } } - if &SPEEDY_TABLE[..] != &table[..] { + if SPEEDY_TABLE[..] != table[..] { panic!( "\n\nSpeedy table outdated, replace with:\n\nstatic SPEEDY_TABLE: [(usize, usize); {}] = {:?};\n\n", table.len(), @@ -219,7 +227,10 @@ fn filter_punct_table(filter: u8) -> &'static [(&'static str, Punctuation)] { } #[inline] -fn filter_punct<'a>(input: &'a [(&'static str, Punctuation)], filter: &[u8]) -> &'a [(&'static str, Punctuation)] { +fn filter_punct<'a>( + input: &'a [(&'static str, Punctuation)], + filter: &[u8], +) -> &'a [(&'static str, Punctuation)] { // requires that PUNCT_TABLE be ordered, shorter entries be first, // and all entries with >1 character also have their prefix in the table let mut start = 0; @@ -273,65 +284,37 @@ impl Token { _ => continue, }; match p { - In | - Eq | - NotEq | - Mod | - And | - BitAndAssign | - AndAssign | - Mul | - Pow | - MulAssign | - Add | - AddAssign | - Sub | - SubAssign | - DivAssign | - Colon | - AssignInto | - Less | - LShift | - LShiftAssign | - LessEq | - LessGreater | - Assign | - Greater | - GreaterEq | - RShift | - RShiftAssign | - QuestionMark | - BitXorAssign | - BitOrAssign | - OrAssign | - Or => return true, - _ => {} + In | Eq | NotEq | Mod | FloatMod | And | BitAndAssign | AndAssign | Mul | Pow + | MulAssign | Add | AddAssign | Sub | SubAssign | DivAssign | Colon + | AssignInto | Less | LShift | LShiftAssign | LessEq | LessGreater | Assign + | Greater | GreaterEq | RShift | RShiftAssign | QuestionMark | BitXorAssign + | BitOrAssign | OrAssign | Or => return true, + _ => {}, } } // space match (prev, self) { - (&Token::Ident(_, true), _) | - (&Token::Punct(Comma), _) => true, - (&Token::Ident(..), &Token::Punct(_)) | - (&Token::Ident(..), &Token::InterpStringEnd(_)) | - (&Token::Ident(..), &Token::InterpStringPart(_)) | - (&Token::Punct(_), &Token::Ident(..)) | - (&Token::InterpStringBegin(_), &Token::Ident(..)) | - (&Token::InterpStringPart(_), &Token::Ident(..)) => false, - (&Token::Ident(..), _) | - (_, &Token::Ident(..)) => true, + (&Token::Ident(_, true), _) | (&Token::Punct(Comma), _) => true, + (&Token::Ident(..), &Token::Punct(_)) + | (&Token::Ident(..), &Token::InterpStringEnd(_)) + | (&Token::Ident(..), &Token::InterpStringPart(_)) + | (&Token::Punct(_), &Token::Ident(..)) + | (&Token::InterpStringBegin(_), &Token::Ident(..)) + | (&Token::InterpStringPart(_), &Token::Ident(..)) => false, + (&Token::Ident(..), _) | (_, &Token::Ident(..)) => true, _ => false, } } /// Check whether this token is whitespace. pub fn is_whitespace(&self) -> bool { - matches!(*self, + matches!( + *self, Token::Punct(Punctuation::Tab) - | Token::Punct(Punctuation::Newline) - | Token::Punct(Punctuation::Space) - | Token::Eof + | Token::Punct(Punctuation::Newline) + | Token::Punct(Punctuation::Space) + | Token::Eof ) } @@ -342,6 +325,14 @@ impl Token { _ => false, } } + + pub fn single_quoted(&self) -> Cow<'static, str> { + match self { + Token::Eof => Cow::Borrowed("EOF"), + Token::Punct(p) => Cow::Borrowed(p.single_quoted()), + _ => Cow::Owned(format!("'{self}'")), + } + } } impl fmt::Display for Token { @@ -349,16 +340,16 @@ impl fmt::Display for Token { use self::Token::*; match *self { Eof => f.write_str("__EOF__"), - Punct(p) => write!(f, "{}", p), + Punct(p) => p.fmt(f), Ident(ref i, _) => f.write_str(i), String(ref i) => Quote(i).fmt(f), - InterpStringBegin(ref i) => write!(f, "\"{}[", i), - InterpStringPart(ref i) => write!(f, "]{}[", i), - InterpStringEnd(ref i) => write!(f, "]{}\"", i), - Resource(ref i) => write!(f, "'{}'", i), + InterpStringBegin(ref i) => write!(f, "\"{i}["), + InterpStringPart(ref i) => write!(f, "]{i}["), + InterpStringEnd(ref i) => write!(f, "]{i}\""), + Resource(ref i) => write!(f, "'{i}'"), Int(i) => FormatFloat(i as f32).fmt(f), Float(i) => FormatFloat(i).fmt(f), - DocComment(ref c) => write!(f, "{}", c), + DocComment(ref c) => c.fmt(f), } } } @@ -372,11 +363,11 @@ impl<'a> fmt::Display for Quote<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = self.0; if s.contains("\"}") { - write!(f, "@@{}@", s) + write!(f, "@@{s}@") } else if s.contains('"') || s.contains('\n') { - write!(f, "{{\"{}\"}}", s) + write!(f, "{{\"{s}\"}}") } else { - write!(f, "\"{}\"", s) + write!(f, "\"{s}\"") } } } @@ -407,13 +398,15 @@ impl fmt::Display for FormatFloat { if exp >= 6.0 || exp <= -5.0 { let n2 = (n * factor).round() * 1.0e-5; let mut precision = 0; - while precision < 5 && (n2 * 10.0f32.powi(precision)) != (n2 * 10.0f32.powi(precision)).round() { + while precision < 5 + && (n2 * 10.0f32.powi(precision)) != (n2 * 10.0f32.powi(precision)).round() + { precision += 1; } write!(f, "{:.*}e{:+04}", precision as usize, n2, exp) } else { let n2 = (n * factor).round() / factor; - write!(f, "{}", n2) + write!(f, "{n2}") } } } @@ -434,11 +427,11 @@ impl LocatedToken { } fn is_digit(ch: u8) -> bool { - ch >= b'0' && ch <= b'9' + ch.is_ascii_digit() } fn is_ident(ch: u8) -> bool { - (ch >= b'a' && ch <= b'z') || (ch >= b'A' && ch <= b'Z') || ch == b'_' + ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == b'_' } fn from_latin1(bytes: &[u8]) -> String { @@ -459,7 +452,7 @@ pub fn from_utf8_or_latin1(bytes: Vec) -> String { } /// Convert the input bytes to a `String` attempting UTF-8 or falling back to Latin-1. -pub fn from_utf8_or_latin1_borrowed(bytes: &[u8]) -> Cow { +pub fn from_utf8_or_latin1_borrowed(bytes: &[u8]) -> Cow<'_, str> { match std::str::from_utf8(bytes) { Ok(v) => Cow::Borrowed(v), Err(_) => Cow::Owned(from_latin1(bytes)), @@ -482,17 +475,12 @@ enum Directive { Stringy, } -fn has_bom(slice: &[u8]) -> bool { - slice.starts_with(b"\xEF\xBB\xBF") -} - fn buffer_read(file: FileId, mut read: R) -> Result, DMError> { let mut buffer = Vec::new(); if let Err(error) = read.read_to_end(&mut buffer) { - let mut tracker = LocationTracker::new(file, buffer.as_slice().into()); - tracker.by_ref().count(); - return Err(DMError::new(tracker.location(), "i/o error reading file").with_cause(error)); + let location = LocationTracker::count_location(file, &buffer); + return Err(DMError::new(location, "i/o error reading file").with_cause(error)); } Ok(buffer) @@ -510,13 +498,22 @@ pub fn buffer_file(file: FileId, path: &std::path::Path) -> Result, DMEr let mut read = match std::fs::File::open(path) { Ok(read) => read, - Err(error) => return Err(DMError::new(Location { file, line: 1, column: 1 }, "i/o error opening file").with_cause(error)), + Err(error) => { + return Err(DMError::new( + Location { + file, + line: 1, + column: 1, + }, + "i/o error opening file", + ) + .with_cause(error)) + }, }; if let Err(error) = read.read_to_end(&mut buffer) { - let mut tracker = LocationTracker::new(file, buffer.as_slice().into()); - tracker.by_ref().count(); - return Err(DMError::new(tracker.location(), "i/o error reading file").with_cause(error)); + let location = LocationTracker::count_location(file, &buffer); + return Err(DMError::new(location, "i/o error reading file").with_cause(error)); } Ok(buffer) @@ -537,8 +534,14 @@ pub struct LocationTracker<'a> { } impl<'a> LocationTracker<'a> { + pub fn count_location(file: FileId, content: &[u8]) -> Location { + let mut tracker = LocationTracker::new(file, content.into()); + tracker.by_ref().count(); + tracker.location() + } + pub fn new(file: FileId, inner: Cow<'a, [u8]>) -> LocationTracker<'a> { - LocationTracker { + let mut this = LocationTracker { inner, offset: 0, location: Location { @@ -547,6 +550,22 @@ impl<'a> LocationTracker<'a> { column: 0, }, at_line_end: true, + }; + // Skip UTF-8 BOM + if this.inner.starts_with(b"\xEF\xBB\xBF") { + this.offset += 3; + } + this + } + + /// `location` will be taken as the location of the first character of `inner`. + pub fn from_location(location: Location, inner: Cow<'a, [u8]>) -> LocationTracker<'a> { + let location = location.pred(); + LocationTracker { + inner, + offset: 0, + location, + at_line_end: location.column == !0, } } @@ -634,19 +653,10 @@ impl<'ctx> HasLocation for Lexer<'ctx> { } impl<'ctx> Lexer<'ctx> { - /// Create a new lexer from a byte stream. - pub fn new>>(context: &'ctx Context, file_number: FileId, input: I) -> Self { - let mut cow = input.into(); - if has_bom(&cow) { - cow = match cow { - Cow::Borrowed(b) => Cow::from(&b[3..]), - Cow::Owned(mut o) => { o.drain(..3); Cow::Owned(o) } - }; - } - + pub fn from_input(context: &'ctx Context, input: LocationTracker<'ctx>) -> Self { Lexer { context, - input: LocationTracker::new(file_number, cow), + input, next: None, final_newline: false, at_line_head: true, @@ -656,8 +666,21 @@ impl<'ctx> Lexer<'ctx> { } } + /// Create a new lexer from a byte stream. + pub fn new>>( + context: &'ctx Context, + file_number: FileId, + input: I, + ) -> Self { + Lexer::from_input(context, LocationTracker::new(file_number, input.into())) + } + /// Create a new lexer from a reader. - pub fn from_read(context: &'ctx Context, file: FileId, read: R) -> Result { + pub fn from_read( + context: &'ctx Context, + file: FileId, + read: R, + ) -> Result { let start_time = std::time::Instant::now(); let input = buffer_read(file, read)?; context.add_io_time(start_time.elapsed()); @@ -665,7 +688,11 @@ impl<'ctx> Lexer<'ctx> { } /// Create a new lexer from a reader. - pub fn from_file(context: &'ctx Context, file: FileId, path: &std::path::Path) -> Result { + pub fn from_file( + context: &'ctx Context, + file: FileId, + path: &std::path::Path, + ) -> Result { let start_time = std::time::Instant::now(); let input = buffer_file(file, path)?; context.add_io_time(start_time.elapsed()); @@ -694,7 +721,7 @@ impl<'ctx> Lexer<'ctx> { self.at_line_head = false; } Some(ch) - } + }, } } @@ -708,6 +735,7 @@ impl<'ctx> Lexer<'ctx> { fn skip_block_comments(&mut self) -> Option { let mut depth = 1; let mut buffer = [0, 0]; + let mut comment_text = Vec::new(); // read the first character and check for being a comment let mut comment = None; @@ -715,14 +743,22 @@ impl<'ctx> Lexer<'ctx> { // '*' must be tracked to accurately end the block comment, and // will be stripped by the documentation parser. Some(b'*') => { - comment = Some(DocComment::new(CommentKind::Block, DocTarget::FollowingItem)); + comment = Some(DocComment::new( + CommentKind::Block, + DocTarget::FollowingItem, + )); buffer[1] = b'*'; - } + }, // '!' will not be skipped by the documentation parser, and is not // important to checking when the block comment has ended. - Some(b'!') => comment = Some(DocComment::new(CommentKind::Block, DocTarget::EnclosingItem)), + Some(b'!') => { + comment = Some(DocComment::new( + CommentKind::Block, + DocTarget::EnclosingItem, + )) + }, Some(other) => buffer[1] = other, - None => {} + None => {}, } loop { @@ -731,9 +767,10 @@ impl<'ctx> Lexer<'ctx> { match self.next() { Some(val) => buffer[1] = val, None => { - self.context.register_error(self.error("still skipping comments at end of file")); + self.context + .register_error(self.error("still skipping comments at end of file")); break; - } + }, } if buffer == *b"/*" { @@ -745,14 +782,15 @@ impl<'ctx> Lexer<'ctx> { } } - if buffer[0] != 0 { - if let Some(ref mut comment) = comment { - comment.text.push(buffer[0] as char); - } + if buffer[0] != 0 && comment.is_some() { + comment_text.push(buffer[0]); } } - comment.filter(|c| !c.text.is_empty()).map(Token::DocComment) + comment.filter(|_| !comment_text.is_empty()).map(|mut c| { + c.text = from_utf8_or_latin1(comment_text); + Token::DocComment(c) + }) } fn skip_line_comment(&mut self) -> Option { @@ -760,31 +798,36 @@ impl<'ctx> Lexer<'ctx> { // read the first character and check for being a comment let mut comment = None; + let mut comment_text = Vec::new(); match self.next() { - Some(b'/') => comment = Some(DocComment::new(CommentKind::Line, DocTarget::FollowingItem)), - Some(b'!') => comment = Some(DocComment::new(CommentKind::Line, DocTarget::EnclosingItem)), + Some(b'/') => { + comment = Some(DocComment::new(CommentKind::Line, DocTarget::FollowingItem)) + }, + Some(b'!') => { + comment = Some(DocComment::new(CommentKind::Line, DocTarget::EnclosingItem)) + }, Some(b'\n') => { self.put_back(Some(b'\n')); return None; - } + }, Some(b'\\') => backslash = true, - _ => {} + _ => {}, } while let Some(ch) = self.next() { - if ch != b'\r' && ch != b'\n' { - if let Some(ref mut comment) = comment { - comment.text.push(ch as char); - } + if ch != b'\r' && ch != b'\n' && comment.is_some() { + comment_text.push(ch); } if ch == b'\r' { // not listening } else if backslash { if ch == b'\n' { - self.error("backslash in line comment may be commenting out the following line") - .set_severity(Severity::Warning) - .register(self.context); + self.error( + "backslash in line comment may be commenting out the following line", + ) + .set_severity(Severity::Warning) + .register(self.context); } backslash = false; } else if ch == b'\n' { @@ -795,7 +838,10 @@ impl<'ctx> Lexer<'ctx> { } } - comment.map(Token::DocComment) + comment.map(|mut c| { + c.text = from_utf8_or_latin1(comment_text); + Token::DocComment(c) + }) } fn read_number_inner(&mut self, first: u8) -> (bool, u32, Cow<'static, str>) { @@ -808,7 +854,7 @@ impl<'ctx> Lexer<'ctx> { if first == b'.' { integer = false; } else if first == b'0' { - radix = 8; // hate. let me tell you... + radix = 8; // hate. let me tell you... match self.next() { Some(b'x') => radix = 16, ch => self.put_back(ch), @@ -825,12 +871,12 @@ impl<'ctx> Lexer<'ctx> { exponent |= ch == b'e' || ch == b'E'; } buf.push(ch as char); - } + }, Some(ch) if (ch == b'+' || ch == b'-') && exponent => { buf.push(ch as char); - } + }, Some(b'#') => { - buf.push('#'); // Keep pushing to `buf` in case of error. + buf.push('#'); // Keep pushing to `buf` in case of error. let start = buf.len(); for _ in 0..3 { if let Some(ch) = self.next() { @@ -850,15 +896,15 @@ impl<'ctx> Lexer<'ctx> { // Got "1.#IND", change it to "NaN" for read_number. return (false, 10, "NaN".into()); } - } + }, Some(ch) if (ch as char).is_digit(::std::cmp::max(radix, 10)) => { exponent = false; buf.push(ch as char); - } + }, ch => { self.put_back(ch); return (integer, radix, buf.into()); - } + }, } } } @@ -877,44 +923,52 @@ impl<'ctx> Lexer<'ctx> { if let Ok(val) = f32::from_str(&buf) { let val_str = val.to_string(); if val_str != buf { - self.error(format!("precision loss of integer constant: \"{}\" to {}", buf, val)) - .set_severity(Severity::Warning) - .with_errortype("integer_precision_loss") - .register(self.context); + self.error(format!( + "precision loss of integer constant: \"{buf}\" to {val}" + )) + .set_severity(Severity::Warning) + .with_errortype("integer_precision_loss") + .register(self.context); } - return Token::Float(val) + return Token::Float(val); } } - self.context.register_error(self.error( - format!("bad base-{} integer \"{}\": {}", radix, buf, original_error))); - Token::Int(0) // fallback + self.context.register_error(self.error(format!( + "bad base-{radix} integer \"{buf}\": {original_error}" + ))); + Token::Int(0) // fallback } else { // ignore radix match f32::from_str(&buf) { Ok(val) => Token::Float(val), Err(e) => { - self.context.register_error(self.error( - format!("bad float \"{}\": {}", buf, e))); - Token::Float(0.0) // fallback - } + self.context + .register_error(self.error(format!("bad float \"{buf}\": {e}"))); + Token::Float(0.0) // fallback + }, } } } - fn read_ident(&mut self, first: u8) -> String { - // 12 is ~89% of idents, 24 is ~99.5%, 48 is ~100% - let mut ident = Vec::with_capacity(12); - ident.push(first); + fn read_ident(&mut self, first: u8) -> (String, bool) { + let start = self.input.offset - 1; + let mut end = start + 1; + assert_eq!(first, self.input.inner[start]); + let ws; loop { match self.next() { - Some(ch) if is_ident(ch) || is_digit(ch) => ident.push(ch), + Some(ch) if is_ident(ch) || is_digit(ch) => { + end += 1; + }, ch => { + ws = ch == Some(b' ') || ch == Some(b'\t'); self.put_back(ch); break; - } + }, } } - from_utf8_or_latin1(ident) + let ident = &self.input.inner[start..end]; + (from_utf8_or_latin1_borrowed(ident).into_owned(), ws) } fn read_resource(&mut self) -> String { @@ -931,9 +985,10 @@ impl<'ctx> Lexer<'ctx> { Some(b'\'') => break, Some(ch) => buf.push(ch), None => { - self.context.register_error(DMError::new(start_loc, "unterminated resource literal")); + self.context + .register_error(DMError::new(start_loc, "unterminated resource literal")); break; - } + }, } } from_utf8_or_latin1(buf) @@ -950,9 +1005,10 @@ impl<'ctx> Lexer<'ctx> { let ch = match self.next() { Some(ch) => ch, None => { - self.context.register_error(DMError::new(start_loc, "unterminated string literal")); + self.context + .register_error(DMError::new(start_loc, "unterminated string literal")); break; - } + }, }; if ch == end[idx] && !backslash { idx += 1; @@ -960,10 +1016,11 @@ impl<'ctx> Lexer<'ctx> { break; } continue; - } else if ch == end[0] && !backslash { - // TODO: this is a hack to fix the '""}' situation - buf.extend_from_slice(&end[..idx]); - idx = 1; + } else if idx > 0 && ch == end[idx - 1] && end[..idx - 1] == end[1..idx] && !backslash { + // Handle `""}` and similar situations by holding `idx` steady + // and shifting the first character of `end` into `buf`. + buf.extend_from_slice(&end[..1]); + continue; } else { buf.extend_from_slice(&end[..idx]); idx = 0; @@ -983,7 +1040,7 @@ impl<'ctx> Lexer<'ctx> { backslash = false; buf.push(b'\\'); buf.push(ch); - } + }, // `backslash` is false hereafter b'[' => { self.interp_stack.push(Interpolation { @@ -992,7 +1049,7 @@ impl<'ctx> Lexer<'ctx> { }); interp_opened = true; break; - } + }, b'\\' => backslash = true, ch => buf.push(ch), } @@ -1014,10 +1071,9 @@ impl<'ctx> Lexer<'ctx> { match self.next() { Some(ch) => buf.push(ch), None => { - DMError::new(start_loc, "unterminated raw string") - .register(self.context); + DMError::new(start_loc, "unterminated raw string").register(self.context); break; - } + }, } if buf.ends_with(terminator) { let len = buf.len() - terminator.len(); @@ -1032,8 +1088,7 @@ impl<'ctx> Lexer<'ctx> { // We just got the '@'. Let's see what the next character is. match self.next() { // @ - error - Some(b'\n') | - None => { + Some(b'\n') | None => { self.error("unterminated raw string").register(self.context); Token::String(String::new()) }, @@ -1046,14 +1101,16 @@ impl<'ctx> Lexer<'ctx> { Some(b')') => break, Some(ch) => terminator.push(ch), None => { - self.error("unterminated raw string terminator").register(self.context); - return Token::String(String::new()) - } + self.error("unterminated raw string terminator") + .register(self.context); + return Token::String(String::new()); + }, } } if terminator.is_empty() { - self.error("empty raw string terminator").register(self.context); - return Token::String(String::new()) + self.error("empty raw string terminator") + .register(self.context); + return Token::String(String::new()); } self.read_raw_string_inner(&terminator) }, @@ -1064,7 +1121,7 @@ impl<'ctx> Lexer<'ctx> { other => { self.put_back(other); self.read_raw_string_inner(b"{") - } + }, }, // @ Some(terminator) => self.read_raw_string_inner(&[terminator]), @@ -1072,7 +1129,7 @@ impl<'ctx> Lexer<'ctx> { } fn read_punct(&mut self, first: u8) -> Option { - let mut needle = [first, 0, 0, 0, 0, 0, 0, 0]; // poor man's StackVec + let mut needle = [first, 0, 0, 0, 0, 0, 0, 0]; // poor man's StackVec let mut needle_idx = 1; let mut items = filter_punct_table(first); @@ -1090,7 +1147,7 @@ impl<'ctx> Lexer<'ctx> { needle[needle_idx] = b; needle_idx += 1; }, - None => return candidate, // EOF + None => return candidate, // EOF } items = filter_punct(items, &needle[..needle_idx]); } @@ -1105,10 +1162,7 @@ impl<'ctx> Lexer<'ctx> { if punct != close { let next = self.next(); match next { - Some(b'\r') | - Some(b' ') | - Some(b'\t') | - Some(b'\n') => {} + Some(b'\r') | Some(b' ') | Some(b'\t') | Some(b'\n') => {}, _ => punct = close, } self.put_back(next); @@ -1121,10 +1175,14 @@ impl<'ctx> Lexer<'ctx> { loop { match self.next() { Some(b'\r') => {}, - Some(b' ') | - Some(b'\t') if !self.at_line_head || skip_newlines > 0 => { self.close_allowed = false; }, - Some(b'\n') if skip_newlines == 2 => { skip_newlines = 1; self.close_allowed = true; }, - ch => return ch + Some(b' ') | Some(b'\t') if !self.at_line_head || skip_newlines > 0 => { + self.close_allowed = false; + }, + Some(b'\n') if skip_newlines == 2 => { + skip_newlines = 1; + self.close_allowed = true; + }, + ch => return ch, } } } @@ -1134,8 +1192,8 @@ impl<'ctx> Iterator for Lexer<'ctx> { type Item = LocatedToken; fn next(&mut self) -> Option { - use self::Token::*; use self::Punctuation::*; + use self::Token::*; let mut skip_newlines = false; let mut found_illegal = false; loop { @@ -1154,7 +1212,7 @@ impl<'ctx> Iterator for Lexer<'ctx> { } else { return None; } - } + }, }; skip_newlines = false; @@ -1175,19 +1233,19 @@ impl<'ctx> Iterator for Lexer<'ctx> { Some(Hash) if self.directive == Directive::None => { self.directive = Directive::Hash; Some(locate(Punct(Hash))) - } + }, Some(BlockComment) => { if let Some(t) = self.skip_block_comments() { return Some(locate(t)); } continue; - } + }, Some(LineComment) => { if let Some(t) = self.skip_line_comment() { return Some(locate(t)); } continue; - } + }, Some(SingleQuote) => Some(locate(Resource(self.read_resource()))), Some(DoubleQuote) => Some(locate(self.read_string(b"\"", false))), Some(BlockString) => Some(locate(self.read_string(b"\"}", false))), @@ -1196,7 +1254,7 @@ impl<'ctx> Iterator for Lexer<'ctx> { interp.bracket_depth += 1; } Some(locate(Punct(lbr))) - } + }, Some(RBracket) => { if let Some(mut interp) = self.interp_stack.pop() { interp.bracket_depth -= 1; @@ -1207,19 +1265,16 @@ impl<'ctx> Iterator for Lexer<'ctx> { } self.close_allowed = true; Some(locate(Punct(RBracket))) - } + }, Some(RParen) => { self.close_allowed = true; Some(locate(Punct(RParen))) - } + }, Some(v) => Some(locate(Punct(v))), None => match first { b'0'..=b'9' => Some(locate(self.read_number(first))), b'_' | b'a'..=b'z' | b'A'..=b'Z' => { - let ident = self.read_ident(first); - let next = self.next(); - self.put_back(next); - let ws = next == Some(b' ') || next == Some(b'\t'); + let (ident, ws) = self.read_ident(first); if self.directive == Directive::Hash { if ident == "warn" || ident == "error" { self.directive = Directive::Stringy; @@ -1228,24 +1283,22 @@ impl<'ctx> Iterator for Lexer<'ctx> { } } // check keywords - for &(name, value) in PUNCT_TABLE.iter() { - if name == ident { - return Some(locate(Punct(value))); - } + if ident == "in" { + return Some(locate(Punct(In))); } self.close_allowed = true; Some(locate(Ident(ident, ws))) - } + }, b'\\' => { self.at_line_head = false; skip_newlines = true; continue; - } + }, b'@' => Some(locate(self.read_raw_string())), _ => { if !found_illegal { - let mut msg = format!("illegal byte 0x{:x}", first); - if first >= b' ' && first <= b'~' { + let mut msg = format!("illegal byte 0x{first:x}"); + if (b' '..=b'~').contains(&first) { use std::fmt::Write; let _ = write!(msg, " ({:?})", first as char); } @@ -1253,7 +1306,7 @@ impl<'ctx> Iterator for Lexer<'ctx> { found_illegal = true; } continue; - } + }, }, }; } diff --git a/crates/dreammaker/src/lib.rs b/crates/dreammaker/src/lib.rs index 2c9b4c6e..50781037 100644 --- a/crates/dreammaker/src/lib.rs +++ b/crates/dreammaker/src/lib.rs @@ -4,43 +4,37 @@ extern crate indexmap; extern crate interval_tree; extern crate lodepng; -#[macro_use] extern crate bitflags; -#[macro_use] extern crate guard; -extern crate termcolor; +#[macro_use] +extern crate bitflags; extern crate ordered_float; extern crate serde; extern crate serde_derive; +extern crate termcolor; extern crate toml; -use std::path::Path; use std::borrow::Cow; - -#[allow(unused_macros)] -macro_rules! try_iter { - ($e:expr) => { - match $e { - Ok(x) => x, - Err(e) => return Some(Err(From::from(e))), - } - }; -} +use std::path::Path; mod error; pub use error::*; +use get_size::GetSize; + +use foldhash::fast::RandomState; +use indexmap::IndexMap; // roughly in order of stage -pub mod docs; -pub mod lexer; -pub mod preprocessor; -pub mod indents; -pub mod parser; pub mod annotation; pub mod ast; -pub mod objtree; mod builtins; +pub mod config; pub mod constants; pub mod dmi; -pub mod config; +pub mod docs; +pub mod indents; +pub mod lexer; +pub mod objtree; +pub mod parser; +pub mod preprocessor; impl Context { /// Run the parsing suite on a given `.dme` file, producing an object tree. @@ -49,10 +43,12 @@ impl Context { /// return a best-effort parse. Call `print_all_errors` to pretty-print /// errors to standard error. pub fn parse_environment(&self, dme: &Path) -> Result { - Ok(parser::parse(self, - indents::IndentProcessor::new(self, - preprocessor::Preprocessor::new(self, dme.to_owned())? - ) + Ok(parser::parse( + self, + indents::IndentProcessor::new( + self, + preprocessor::Preprocessor::new(self, dme.to_owned())?, + ), )) } } @@ -64,9 +60,10 @@ impl Context { /// /// If `show_ws` is true, braces and semicolons are included directly in the /// output rather than only being implied by the indentation. -pub fn pretty_print(w: &mut W, input: I, show_ws: bool) -> std::fmt::Result where +pub fn pretty_print(w: &mut W, input: I, show_ws: bool) -> std::fmt::Result +where W: std::fmt::Write, - I: IntoIterator + I: IntoIterator, { let mut indents = 0; let mut needs_newline = false; @@ -79,29 +76,29 @@ pub fn pretty_print(w: &mut W, input: I, show_ws: bool) -> std::fmt::Resul if show_ws { write!(w, "{{")?; } - } + }, lexer::Token::Punct(lexer::Punctuation::RBrace) => { indents -= 1; needs_newline = true; if show_ws { write!(w, "}}")?; } - } - lexer::Token::Punct(lexer::Punctuation::Semicolon) | - lexer::Token::Punct(lexer::Punctuation::Newline) => { + }, + lexer::Token::Punct(lexer::Punctuation::Semicolon) + | lexer::Token::Punct(lexer::Punctuation::Newline) => { needs_newline = true; if show_ws { write!(w, ";")?; } - } - lexer::Token::DocComment(_) => {} + }, + lexer::Token::DocComment(_) => {}, other => { if needs_newline { const SPACES: &str = " "; let spaces = 2 * indents; writeln!(w)?; for _ in 0..(spaces / SPACES.len()) { - write!(w, "{}", SPACES)?; + write!(w, "{SPACES}")?; } write!(w, "{}", &SPACES[..spaces % SPACES.len()])?; needs_newline = false; @@ -110,9 +107,9 @@ pub fn pretty_print(w: &mut W, input: I, show_ws: bool) -> std::fmt::Resul write!(w, " ")?; } } - write!(w, "{}", other)?; + write!(w, "{other}")?; prev = Some(other); - } + }, } } if needs_newline { @@ -129,7 +126,7 @@ pub fn pretty_print(w: &mut W, input: I, show_ws: bool) -> std::fmt::Resul /// On Windows, this is a no-op. #[cfg(windows)] #[inline(always)] -pub fn fix_case(path: &Path) -> Cow { +pub fn fix_case(path: &Path) -> Cow<'_, Path> { Cow::Borrowed(path) } @@ -138,7 +135,7 @@ pub fn fix_case(path: &Path) -> Cow { /// On non-Windows platforms, the parent of the given path is searched for a /// file with the same name but a different case. #[cfg(not(windows))] -pub fn fix_case(path: &Path) -> Cow { +pub fn fix_case(path: &Path) -> Cow<'_, Path> { if path.exists() { return Cow::Borrowed(path); } @@ -171,20 +168,21 @@ pub const DEFAULT_ENV: &str = "tgstation.dme"; /// Autodetect any `.dme` file in the current folder, or fall back to default. /// /// If multiple environments exist, the first non-default is preferred. -pub fn detect_environment(root: &Path, default: &str) -> std::io::Result> { +pub fn detect_environment( + root: &Path, + default: &str, +) -> std::io::Result> { let mut result = None; - for entry in std::fs::read_dir(root)? { - if let Ok(entry) = entry { - let name = entry.file_name(); - let (dme, default) = { - let utf8_name = name.to_string_lossy(); - (utf8_name.ends_with(".dme"), utf8_name == default) - }; - if dme { - result = Some(entry.path()); - if !default { - break; - } + for entry in std::fs::read_dir(root)?.flatten() { + let name = entry.file_name(); + let (dme, default) = { + let utf8_name = name.to_string_lossy(); + (utf8_name.ends_with(".dme"), utf8_name == default) + }; + if dme { + result = Some(entry.path()); + if !default { + break; } } } @@ -193,8 +191,32 @@ pub fn detect_environment(root: &Path, default: &str) -> std::io::Result std::io::Result> { // Return a path in the current directory `.` ... - detect_environment(".".as_ref(), DEFAULT_ENV).map(|o| o.map(|path| { - // ... but without `./` preceding it. - path.strip_prefix(".").map(|p| p.to_owned()).unwrap_or(path) - })) + detect_environment(".".as_ref(), DEFAULT_ENV).map(|o| { + o.map(|path| { + // ... but without `./` preceding it. + path.strip_prefix(".").map(|p| p.to_owned()).unwrap_or(path) + }) + }) +} + +fn heap_size_of_index_map(index_map: &IndexMap) -> usize +where + K: GetSize, + V: GetSize, +{ + let mut total = 0; + + for (k, v) in index_map.iter() { + // We assume that keys and value are hold inside the heap. + total += GetSize::get_size(k); + total += GetSize::get_size(v); + } + + let additional: usize = index_map.capacity() - index_map.len(); + total += additional * K::get_stack_size(); + total += additional * V::get_stack_size(); + + total += u64::get_stack_size() * 4; // composition of RandomState + + total } diff --git a/crates/dreammaker/src/objtree.rs b/crates/dreammaker/src/objtree.rs index 277853f6..9763189f 100644 --- a/crates/dreammaker/src/objtree.rs +++ b/crates/dreammaker/src/objtree.rs @@ -2,14 +2,24 @@ use std::collections::BTreeMap; use std::fmt; +use std::mem::size_of; +use std::ops::Range; +use get_size::GetSize; +use get_size_derive::GetSize; + +use foldhash::fast::RandomState; use indexmap::IndexMap; -use ahash::RandomState; -use super::ast::{Expression, VarType, VarTypeBuilder, VarSuffix, PathOp, Parameter, Block, ProcDeclKind, Ident}; +use crate::heap_size_of_index_map; + +use super::ast::{ + Block, Expression, Ident, Parameter, PathOp, ProcDeclBuilder, ProcDeclKind, ProcFlags, + ProcReturnType, VarSuffix, VarType, VarTypeBuilder, +}; use super::constants::Constant; use super::docs::DocCollection; -use super::{DMError, Location, Context, Severity}; +use super::{Context, DMError, Location, Severity}; // ---------------------------------------------------------------------------- // Symbol IDs @@ -18,6 +28,8 @@ use super::{DMError, Location, Context, Severity}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct SymbolId(u32); +impl GetSize for SymbolId {} + #[derive(Debug)] pub struct SymbolIdSource(SymbolId); @@ -47,14 +59,14 @@ impl SymbolIdSource { pub type Vars = IndexMap; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, GetSize)] pub struct VarDeclaration { pub var_type: VarType, pub location: Location, pub id: SymbolId, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, GetSize)] pub struct VarValue { pub location: Location, /// Syntactic value, as specified in the source. @@ -65,30 +77,37 @@ pub struct VarValue { pub docs: DocCollection, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, GetSize)] pub struct TypeVar { pub value: VarValue, pub declaration: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, GetSize)] pub struct ProcDeclaration { pub location: Location, pub kind: ProcDeclKind, + // todo: tie this into our return type, add support for the funky types + pub return_type: ProcReturnType, + pub flags: ProcFlags, pub id: SymbolId, - pub is_private: bool, - pub is_protected: bool, } -#[derive(Debug, Clone)] +fn heap_size_of_location_range(_range: &Option>) -> usize { + size_of::>() +} + +#[derive(Debug, Clone, GetSize)] pub struct ProcValue { pub location: Location, pub parameters: Box<[Parameter]>, pub docs: DocCollection, pub code: Option, + #[get_size(size_fn = heap_size_of_location_range)] + pub body_range: Option>, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, GetSize)] pub struct TypeProc { pub value: Vec, pub declaration: Option, @@ -104,15 +123,17 @@ impl TypeProc { // ---------------------------------------------------------------------------- // Types -#[derive(Debug)] +#[derive(Debug, GetSize)] pub struct Type { pub path: String, path_last_slash: usize, pub location: Location, location_specificity: usize, /// Variables which this type has declarations or overrides for. + #[get_size(size_fn = heap_size_of_index_map)] pub vars: IndexMap, /// Procs and verbs which this type has declarations or overrides for. + #[get_size(size_fn = heap_size_of_index_map)] pub procs: IndexMap, parent_path: NodeIndex, parent_type: NodeIndex, @@ -160,12 +181,16 @@ impl Type { /// Checks whether this type's path is a subpath of the given path. #[inline] pub fn is_subpath_of(&self, parent: &str) -> bool { - subpath(&self.path, parent) + ispath(&self.path, parent) } // Used in the constant evaluator which holds an &mut ObjectTree and thus // can't be used with TypeRef. - pub(crate) fn get_value<'a>(&'a self, name: &str, objtree: &'a ObjectTree) -> Option<&'a VarValue> { + pub(crate) fn get_value<'a>( + &'a self, + name: &str, + objtree: &'a ObjectTree, + ) -> Option<&'a VarValue> { let mut current = Some(self); while let Some(ty) = current { if let Some(var) = ty.vars.get(name) { @@ -176,7 +201,11 @@ impl Type { None } - pub(crate) fn get_var_declaration<'a>(&'a self, name: &str, objtree: &'a ObjectTree) -> Option<&'a VarDeclaration> { + pub(crate) fn get_var_declaration<'a>( + &'a self, + name: &str, + objtree: &'a ObjectTree, + ) -> Option<&'a VarDeclaration> { let mut current = Some(self); while let Some(ty) = current { if let Some(var) = ty.vars.get(name) { @@ -191,9 +220,13 @@ impl Type { } #[inline] -pub fn subpath(path: &str, parent: &str) -> bool { - debug_assert!(path.starts_with('/') && parent.starts_with('/') && parent.ends_with('/')); - path == &parent[..parent.len() - 1] || path.starts_with(parent) +pub fn ispath(path: &str, parent: &str) -> bool { + debug_assert!(path.starts_with('/') && parent.starts_with('/')); + let parent = parent.trim_end_matches('/'); + match path.strip_prefix(parent) { + Some(rest) => rest.is_empty() || rest.starts_with('/'), + None => false, + } } // ---------------------------------------------------------------------------- @@ -236,7 +269,10 @@ impl<'a> TypeRef<'a> { /// Find the parent **type** based on `parent_type` var, or parent path if unspecified. pub fn parent_type(&self) -> Option> { let idx = self.parent_type; - self.tree.graph.get(idx.index()).map(|_| TypeRef::new(self.tree, idx)) + self.tree + .graph + .get(idx.index()) + .map(|_| TypeRef::new(self.tree, idx)) } /// Find the parent type of this without returning root. @@ -245,17 +281,24 @@ impl<'a> TypeRef<'a> { if idx == NodeIndex::new(0) { return None; } - self.tree.graph.get(idx.index()).map(|_| TypeRef::new(self.tree, idx)) + self.tree + .graph + .get(idx.index()) + .map(|_| TypeRef::new(self.tree, idx)) } /// Find a child **path** with the given name, if it exists. pub fn child(&self, name: &str) -> Option> { - self.children.get(name).map(|&idx| TypeRef::new(self.tree, idx)) + self.children + .get(name) + .map(|&idx| TypeRef::new(self.tree, idx)) } /// Iterate over all child **paths**. - pub fn children<'b>(&'b self) -> impl Iterator> + 'b { - self.children.values().map(move |&idx| TypeRef::new(self.tree, idx)) + pub fn children<'b>(&'b self) -> impl Iterator> + 'b { + self.children + .values() + .map(move |&idx| TypeRef::new(self.tree, idx)) } /// Recursively visit this and all child **paths**. @@ -275,7 +318,7 @@ impl<'a> TypeRef<'a> { } } - pub fn iter_parent_types(&self) -> impl Iterator> { + pub fn iter_parent_types(&self) -> impl Iterator> { struct ParentTypeIter<'a>(Option>); impl<'a> Iterator for ParentTypeIter<'a> { type Item = TypeRef<'a>; @@ -323,7 +366,8 @@ impl<'a> TypeRef<'a> { return Some(child); } for &idx in self.children.values() { - if let Some(child) = TypeRef::new(self.tree, idx).navigate(PathOp::Colon, name) { + if let Some(child) = TypeRef::new(self.tree, idx).navigate(PathOp::Colon, name) + { // Yes, simply returning the first thing that matches // is the correct behavior. return Some(child); @@ -335,7 +379,10 @@ impl<'a> TypeRef<'a> { } /// Find another type relative to this type. - pub fn navigate_path>(self, pieces: &[(PathOp, S)]) -> Option> { + pub fn navigate_path>( + self, + pieces: &[(PathOp, S)], + ) -> Option> { let mut next = Some(self); if let Some(&(PathOp::Slash, _)) = pieces.first() { next = Some(self.tree.root()); @@ -424,7 +471,7 @@ impl<'a> TypeRef<'a> { None } - pub fn iter_self_procs(self) -> impl Iterator> { + pub fn iter_self_procs(self) -> impl Iterator> { self.get().procs.iter().flat_map(move |(name, type_proc)| { let list = &type_proc.value; (0..list.len()).map(move |idx| ProcRef { @@ -487,14 +534,20 @@ impl<'o> NavigatePathResult<'o> { } pub fn to_path(self) -> Vec { - let mut path: Vec = self.ty().path.split('/').skip(1).map(ToOwned::to_owned).collect(); + let mut path: Vec = self + .ty() + .path + .split('/') + .skip(1) + .map(ToOwned::to_owned) + .collect(); match self { NavigatePathResult::Type(_) => {}, NavigatePathResult::ProcGroup(_, kind) => path.push(kind.to_string()), NavigatePathResult::ProcPath(proc, kind) => { path.push(kind.to_string()); path.push(proc.name().to_owned()); - } + }, } path } @@ -591,7 +644,14 @@ impl<'a> std::ops::Deref for ProcRef<'a> { impl<'a> fmt::Debug for ProcRef<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}/proc/{}[{}/{}]", self.ty, self.name, self.idx, self.list.len()) + write!( + f, + "{:?}/proc/{}[{}/{}]", + self.ty, + self.name, + self.idx, + self.list.len() + ) } } @@ -624,7 +684,7 @@ impl<'a> std::hash::Hash for ProcRef<'a> { // ---------------------------------------------------------------------------- // The object tree itself -#[derive(Debug, Default)] +#[derive(Debug, Default, GetSize)] pub struct ObjectTree { graph: Vec, types: BTreeMap, @@ -640,29 +700,29 @@ impl ObjectTree { // ------------------------------------------------------------------------ // Access - pub fn node_indices(&self) -> impl Iterator { + pub fn node_indices(&self) -> impl Iterator { (0..self.graph.len()).map(NodeIndex::new) } - pub fn iter_types<'a>(&'a self) -> impl Iterator> + 'a { + pub fn iter_types(&self) -> impl Iterator> + '_ { self.node_indices().map(move |idx| TypeRef::new(self, idx)) } - pub fn root(&self) -> TypeRef { + pub fn root(&self) -> TypeRef<'_> { TypeRef::new(self, NodeIndex::new(0)) } - pub fn find(&self, path: &str) -> Option { + pub fn find(&self, path: &str) -> Option> { if path.is_empty() { return Some(self.root()); } self.types.get(path).map(|&ix| TypeRef::new(self, ix)) } - pub fn expect(&self, path: &str) -> TypeRef { + pub fn expect(&self, path: &str) -> TypeRef<'_> { match self.types.get(path) { Some(&ix) => TypeRef::new(self, ix), - None => panic!("type not found: {:?}", path), + None => panic!("type not found: {path:?}"), } } @@ -670,7 +730,7 @@ impl ObjectTree { self.graph.get(type_.parent_type.index()) } - pub fn type_by_path(&self, path: I) -> Option + pub fn type_by_path(&self, path: I) -> Option> where I: IntoIterator, I::Item: AsRef, @@ -683,7 +743,7 @@ impl ObjectTree { } } - pub fn type_by_path_approx(&self, path: I) -> (bool, TypeRef) + pub fn type_by_path_approx(&self, path: I) -> (bool, TypeRef<'_>) where I: IntoIterator, I::Item: AsRef, @@ -707,7 +767,7 @@ impl ObjectTree { (true, TypeRef::new(self, current)) } - pub fn type_by_constant(&self, constant: &Constant) -> Option { + pub fn type_by_constant(&self, constant: &Constant) -> Option> { match constant { Constant::String(string_path) => self.find(string_path), Constant::Prefab(pop) => self.type_by_path(pop.path.iter()), @@ -737,7 +797,9 @@ impl std::ops::Index for ObjectTree { impl std::ops::IndexMut for ObjectTree { fn index_mut(&mut self, ix: NodeIndex) -> &mut Type { - self.graph.get_mut(ix.index()).expect("node index out of range") + self.graph + .get_mut(ix.index()) + .expect("node index out of range") } } @@ -812,7 +874,15 @@ impl ObjectTreeBuilder { fn assign_parent_types(&mut self, context: &Context) { for (path, &type_idx) in self.inner.types.iter() { let mut location = self.inner[type_idx].location; - let idx = if path == "/datum" || path == "/list" || path == "/savefile" || path == "/world" { + let idx = if path == "/datum" + || path == "/list" + || path == "/alist" + || path == "/savefile" + || path == "/world" + || path == "/vector" + || path == "/pixloc" + || path == "/callee" + { // These types have no parent and cannot have one added. In the official compiler: // - setting list or savefile/parent_type is denied with the same error as setting something's parent type to them; // - setting datum/parent_type infinite loops the compiler; @@ -824,7 +894,7 @@ impl ObjectTreeBuilder { if var.value.expression.is_some() { context.register_error(DMError::new( var.value.location, - format!("not allowed to change {}/parent_type", path), + format!("not allowed to change {path}/parent_type"), )); } } @@ -858,7 +928,7 @@ impl ObjectTreeBuilder { Ok(constant) => { constant_buf = constant; Ok(&constant_buf) - } + }, Err(e) => Err(e), } } else if path == "/client" { @@ -866,33 +936,36 @@ impl ObjectTreeBuilder { Ok(&empty_string) } else { // A weird situation which should not happen. - Err(DMError::new(location, format!("missing {}/parent_type", path))) + Err(DMError::new( + location, + format!("missing {path}/parent_type"), + )) }; match constant { Ok(Constant::String(s)) => { parent_type = s; - } + }, Ok(Constant::Prefab(ref pop)) if pop.vars.is_empty() => { parent_type_buf = String::new(); for piece in pop.path.iter() { parent_type_buf.push('/'); - parent_type_buf.push_str(&piece); + parent_type_buf.push_str(piece); } parent_type = &parent_type_buf; - } + }, Ok(other) => { - context.register_error(DMError::new(location, format!("value of {}/parent_type must be a string or typepath, got {}", path, other))); - } + context.register_error(DMError::new(location, format!("value of {path}/parent_type must be a string or typepath, got {other}"))); + }, Err(e) => { context.register_error(e); - } + }, } } parent_type }; - if path == "/client" && parent_type == "" { + if path == "/client" && parent_type.is_empty() { // client has no parent by default, but can be safely reparented to /datum NodeIndex::new(0) } else if let Some(&idx) = self.inner.types.get(parent_type) { @@ -900,9 +973,9 @@ impl ObjectTreeBuilder { } else { context.register_error(DMError::new( location, - format!("bad parent type for {}: {}", path, parent_type), + format!("bad parent type for {path}: {parent_type}"), )); - NodeIndex::new(0) // on bad parent_type, fall back to the root + NodeIndex::new(0) // on bad parent_type, fall back to the root } }; @@ -913,7 +986,13 @@ impl ObjectTreeBuilder { // ------------------------------------------------------------------------ // Parsing - pub(crate) fn subtype_or_add(&mut self, location: Location, parent: NodeIndex, child: &str, len: usize) -> NodeIndex { + pub(crate) fn subtype_or_add( + &mut self, + location: Location, + parent: NodeIndex, + child: &str, + len: usize, + ) -> NodeIndex { if let Some(&target) = self.inner[parent].children.get(child) { let node = &mut self.inner[target]; if node.location_specificity > len { @@ -953,9 +1032,7 @@ impl ObjectTreeBuilder { ) -> &mut TypeVar { // TODO: warn and merge docs for repeats match self.inner[ty].vars.entry(name.to_owned()) { - indexmap::map::Entry::Vacant(slot) => { - slot.insert(TypeVar { value, declaration }) - }, + indexmap::map::Entry::Vacant(slot) => slot.insert(TypeVar { value, declaration }), indexmap::map::Entry::Occupied(slot) => { let type_var = slot.into_mut(); if let Some(declaration) = declaration { @@ -977,17 +1054,22 @@ impl ObjectTreeBuilder { expression: Option, ) -> &mut TypeVar { let id = self.symbols.allocate(); - self.insert_var(ty, name, VarValue { - location, - expression, - docs, - constant: None, - being_evaluated: false, - }, Some(VarDeclaration { - var_type, - location, - id, - })) + self.insert_var( + ty, + name, + VarValue { + location, + expression, + docs, + constant: None, + being_evaluated: false, + }, + Some(VarDeclaration { + var_type, + location, + id, + }), + ) } pub(crate) fn override_var( @@ -998,16 +1080,21 @@ impl ObjectTreeBuilder { docs: DocCollection, expression: Expression, ) -> &mut TypeVar { - self.insert_var(ty, name, VarValue { - location, - expression: Some(expression), - docs, - constant: None, - being_evaluated: false, - }, None) + self.insert_var( + ty, + name, + VarValue { + location, + expression: Some(expression), + docs, + constant: None, + being_evaluated: false, + }, + None, + ) } - fn get_from_path<'a, I: Iterator>( + fn get_from_path<'a, I: Iterator>( &mut self, location: Location, mut path: I, @@ -1042,7 +1129,7 @@ impl ObjectTreeBuilder { suffix: VarSuffix, ) -> Result, DMError> where - I: Iterator, + I: Iterator, { use super::ast::VarTypeFlags; let mut is_declaration = false; @@ -1074,59 +1161,72 @@ impl ObjectTreeBuilder { let mut var_type = VarTypeBuilder { flags, type_path, + input_type: None, }; var_type.suffix(&suffix); let symbols = &mut self.symbols; let node = &mut self.inner.graph[parent.index()]; // TODO: warn and merge docs for repeats - Ok(Some(node.vars.entry(prev.to_owned()).or_insert_with(|| TypeVar { - value: VarValue { - location, - expression: suffix.into_initializer(), - constant: None, - being_evaluated: false, - docs: comment, - }, - declaration: if is_declaration { - Some(VarDeclaration { - var_type: var_type.build(), + Ok(Some(node.vars.entry(prev.to_owned()).or_insert_with( + || TypeVar { + value: VarValue { location, - id: symbols.allocate(), - }) - } else { - None + expression: suffix.into_initializer(), + constant: None, + being_evaluated: false, + docs: comment, + }, + declaration: if is_declaration { + Some(VarDeclaration { + var_type: var_type.build(), + location, + id: symbols.allocate(), + }) + } else { + None + }, }, - }))) + ))) } + // It's fine. + #[allow(clippy::too_many_arguments)] pub(crate) fn register_proc( &mut self, context: &Context, location: Location, parent: NodeIndex, name: &str, - declaration: Option, + declaration: Option, parameters: Vec, + return_type: ProcReturnType, code: Option, + body_range: Option>, ) -> Result<(usize, &mut ProcValue), DMError> { let node = &mut self.inner.graph[parent.index()]; - let proc = node.procs.entry(name.to_owned()).or_insert_with(|| TypeProc { - value: Vec::with_capacity(1), - declaration: None, - }); - if let Some(kind) = declaration { + let proc = node + .procs + .entry(name.to_owned()) + .or_insert_with(|| TypeProc { + value: Vec::with_capacity(1), + declaration: None, + }); + if let Some(decl_builder) = declaration { if let Some(ref decl) = proc.declaration { - DMError::new(location, format!("duplicate definition of {}/{}", kind, name)) - .with_note(decl.location, "previous definition") - .register(context); + DMError::new( + location, + format!("duplicate definition of {}/{}", decl_builder.kind, name), + ) + .with_note(decl.location, "previous definition") + .register(context); } else { proc.declaration = Some(ProcDeclaration { location, - kind, + kind: decl_builder.kind, + flags: decl_builder.flags, + return_type, id: self.symbols.allocate(), - is_private: false, - is_protected: false, }); } } @@ -1135,7 +1235,8 @@ impl ObjectTreeBuilder { location, parameters: parameters.into(), docs: Default::default(), - code + code, + body_range, }; // DM really does reorder the declaration to appear before the override, @@ -1150,31 +1251,35 @@ impl ObjectTreeBuilder { // Show the hint now, make up for it by putting the original // at the beginning of the list (so `..()` finds it). // Configuration can be used to upgrade this above a hint. - DMError::new(proc.value[0].location, format!("override of {}/{} precedes definition", node.path, name)) - .set_severity(Severity::Hint) - .with_errortype("override_precedes_definition") - .with_note(location, format!("{}/{}/{} is defined here", node.path, decl, name)) - .register(context); + DMError::new( + proc.value[0].location, + format!("override of {}/{} precedes definition", node.path, name), + ) + .set_severity(Severity::Hint) + .with_errortype("override_precedes_definition") + .with_note( + location, + format!("{}/{}/{} is defined here", node.path, decl, name), + ) + .register(context); proc.value.insert(0, value); Ok((len, proc.value.first_mut().unwrap())) }, _ => { proc.value.push(value); Ok((len, proc.value.last_mut().unwrap())) - } + }, } } - pub(crate) fn add_builtin_type( - &mut self, - elems: &[&'static str], - ) -> &mut Type { + pub(crate) fn add_builtin_type(&mut self, elems: &[&'static str]) -> &mut Type { self.add_type( Location::builtins(), elems.iter().cloned(), elems.len() + 1, Default::default(), - ).unwrap() + ) + .unwrap() } // an entry which may be anything depending on the path @@ -1202,7 +1307,17 @@ impl ObjectTreeBuilder { let len = elems.len() + 1; let (parent, initial) = self.get_from_path(location, &mut path, len).unwrap(); - if let Some(type_var) = self.register_var(location, parent, initial, path, Default::default(), Default::default()).unwrap() { + if let Some(type_var) = self + .register_var( + location, + parent, + initial, + path, + Default::default(), + Default::default(), + ) + .unwrap() + { type_var.value.location = location; type_var.value.constant = value; &mut type_var.value @@ -1221,12 +1336,23 @@ impl ObjectTreeBuilder { Location::builtins(), elems.iter().copied(), elems.len() + 1, - params.iter().copied().map(|param| Parameter { name: param.into(), .. Default::default() }).collect(), + params + .iter() + .copied() + .map(|param| Parameter { + name: param.into(), + ..Default::default() + }) + .collect(), None, - ).unwrap().1 + None, + ) + .unwrap() + .1 } // an entry which is definitely a proc because an argument list is specified + #[allow(clippy::too_many_arguments)] fn add_proc<'a, I: Iterator>( &mut self, context: &Context, @@ -1235,12 +1361,19 @@ impl ObjectTreeBuilder { len: usize, parameters: Vec, code: Option, + body_range: Option>, ) -> Result<(usize, &mut ProcValue), DMError> { let (parent, mut proc_name) = self.get_from_path(location, &mut path, len)?; let mut declaration = None; if let Some(kind) = ProcDeclKind::from_name(proc_name) { - declaration = Some(kind); - proc_name = match path.next() { + let mut next_entry = path.next(); + let flags = ProcFlags::from_name(next_entry.unwrap_or("")); + if flags.is_some() { + // did something? take another step + next_entry = path.next(); + } + declaration = Some(ProcDeclBuilder::new(kind, flags)); + proc_name = match next_entry { Some(name) => name, None => return Err(DMError::new(location, "proc must have a name")), }; @@ -1250,11 +1383,21 @@ impl ObjectTreeBuilder { if let Some(other) = path.next() { return Err(DMError::new( location, - format!("proc name must be a single identifier (spurious {:?})", other), + format!("proc name must be a single identifier (spurious {other:?})"), )); } - self.register_proc(context, location, parent, proc_name, declaration, parameters, code) + self.register_proc( + context, + location, + parent, + proc_name, + declaration, + parameters, + ProcReturnType::default(), + code, + body_range, + ) } } @@ -1290,6 +1433,8 @@ impl NodeIndex { #[inline] pub fn end() -> Self { - NodeIndex(std::u32::MAX) + NodeIndex(u32::MAX) } } + +impl GetSize for NodeIndex {} diff --git a/crates/dreammaker/src/parser.rs b/crates/dreammaker/src/parser.rs index e32ac05a..7335e27a 100644 --- a/crates/dreammaker/src/parser.rs +++ b/crates/dreammaker/src/parser.rs @@ -1,15 +1,16 @@ //! Minimalist parser which turns a token stream into an object tree. use std::borrow::Cow; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, VecDeque}; use std::ops::Range; +use std::str::FromStr; -use super::{DMError, Location, HasLocation, Context, Severity, FileId}; -use super::lexer::{LocatedToken, Token, Punctuation}; -use super::objtree::{ObjectTreeBuilder, ObjectTree, NodeIndex}; use super::annotation::*; use super::ast::*; use super::docs::*; +use super::lexer::{LocatedToken, Punctuation, Token}; +use super::objtree::{NodeIndex, ObjectTree, ObjectTreeBuilder}; +use super::{Context, DMError, FileId, HasLocation, Location, Severity}; // ---------------------------------------------------------------------------- // Error handling @@ -24,6 +25,11 @@ fn success(t: T) -> Status { Ok(Some(t)) } +#[inline] +fn try_another() -> Status { + Ok(None) +} + const SUCCESS: Status<()> = Ok(Some(())); macro_rules! require { @@ -37,11 +43,48 @@ macro_rules! leading { ($e:expr) => { match $e? { Some(x) => x, - None => return Ok(None), + None => return try_another(), } }; } +macro_rules! take_match { + ( + $self:ident { + $( + $p:pat $( if $condition:expr )? => $branch:expr, + )* + } + // else mandatory because you should never be matching on every option + else $else:expr + ) => { + // Both $p introduce bindings. The first set are by-ref, the second set + // are by-move. It'd be nice to warn if *both* were unused, but warning + // when just one is unused is too much, so don't warn. + { + #[allow(unused_variables)] + match $self.peek() { + $( + $p $( if $condition )? => { + // inner match that moves instead of refs + match $self.take() { + // no duplicate `if` because types are different + $p => { + #[warn(unused_variables)] + $branch + } + // might be unreachable because of missing `if` + #[allow(unreachable_patterns)] + _ => panic!("take_match inner match failed somehow"), + } + } + )* + _ => $else + } + } + } +} + // ---------------------------------------------------------------------------- // Convenience functions @@ -52,20 +95,24 @@ macro_rules! leading { /// be registered with the provided `Context`. pub fn parse(context: &Context, iter: I) -> ObjectTree where - I: IntoIterator, + I: IntoIterator, { - Parser::new(context, iter.into_iter()).parse_object_tree() + Parser::new(context, iter).parse_object_tree() } /// Parse a token stream into an expression. /// /// Fatal errors will be directly returned and miscellaneous diagnostics will /// be registered with the provided `Context`. -pub fn parse_expression(context: &Context, location: Location, iter: I) -> Result +pub fn parse_expression( + context: &Context, + location: Location, + iter: I, +) -> Result where - I: IntoIterator, + I: IntoIterator, { - let mut parser = Parser::new(context, iter.into_iter()); + let mut parser = Parser::new(context, iter); parser.location = location; Ok(require!(parser.expression())) } @@ -90,6 +137,8 @@ impl OpInfo { } #[derive(Debug, Clone, Copy)] +// Too much effort to change now, and it matches the ast naming, so whatever +#[allow(clippy::enum_variant_names)] enum Op { BinaryOp(BinaryOp), AssignOp(AssignOp), @@ -145,11 +194,12 @@ oper_table! { BINARY_OPS; Pow { (BinaryOp, Pow), } - // * / % + // * / % %% Mul { (BinaryOp, Mul), // (BinaryOp, Div = Slash), // (BinaryOp, Mod), + (BinaryOp, FloatMod), } // + - Add { @@ -162,6 +212,7 @@ oper_table! { BINARY_OPS; (BinaryOp, Greater), (BinaryOp, LessEq), (BinaryOp, GreaterEq), + (BinaryOp, LessOrGreater), } // << >> Shift { @@ -200,7 +251,7 @@ oper_table! { BINARY_OPS; Conditional { (TernaryOp, Conditional = QuestionMark), } - // = += -= -= *= /= %= &= |= ^= <<= >>= + // = += -= -= *= /= %= %%= &= |= ^= <<= >>= Assign { (AssignOp, Assign), (AssignOp, AddAssign), @@ -208,6 +259,7 @@ oper_table! { BINARY_OPS; (AssignOp, MulAssign), (AssignOp, DivAssign), (AssignOp, ModAssign), + (AssignOp, FloatModAssign), (AssignOp, BitAndAssign), (AssignOp, BitOrAssign), (AssignOp, BitXorAssign), @@ -250,14 +302,22 @@ impl TTKind { } } - fn is_end(self, token: &Token) -> bool { - match (self, token) { - (TTKind::Paren, &Token::Punct(Punctuation::RParen)) => true, - (TTKind::Brace, &Token::Punct(Punctuation::RBrace)) => true, - (TTKind::Bracket, &Token::Punct(Punctuation::RBracket)) => true, - _ => false, + fn end(self) -> &'static str { + match self { + TTKind::Paren => "')'", + TTKind::Brace => "'}'", + TTKind::Bracket => "']'", } } + + fn is_end(self, token: &Token) -> bool { + matches!( + (self, token), + (TTKind::Paren, &Token::Punct(Punctuation::RParen)) + | (TTKind::Brace, &Token::Punct(Punctuation::RBrace)) + | (TTKind::Bracket, &Token::Punct(Punctuation::RBracket)) + ) + } } // ---------------------------------------------------------------------------- @@ -284,17 +344,16 @@ pub struct Parser<'ctx, 'an, 'inp> { tree: ObjectTreeBuilder, fatal_errored: bool, - input: Box + 'inp>, + input: Box + 'inp>, eof: bool, possible_indentation_error: bool, next: Option, location: Location, expected: Vec>, + skipping_location: Option, - docs_following: DocCollection, - docs_enclosing: DocCollection, + doc_comments_pending: VecDeque<(Location, DocComment)>, module_docs: BTreeMap>, - in_docs: usize, procs: bool, procs_bad: u64, @@ -309,7 +368,10 @@ impl<'ctx, 'an, 'inp> HasLocation for Parser<'ctx, 'an, 'inp> { impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { /// Construct a new parser using the given input stream. - pub fn new + 'inp>(context: &'ctx Context, input: I) -> Self { + pub fn new + 'inp>( + context: &'ctx Context, + input: I, + ) -> Self { Parser { context, annotations: None, @@ -322,11 +384,10 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { next: None, location: Default::default(), expected: Vec::new(), + skipping_location: None, - docs_following: Default::default(), - docs_enclosing: Default::default(), + doc_comments_pending: Default::default(), module_docs: Default::default(), - in_docs: 0, procs: false, procs_bad: 0, @@ -361,7 +422,9 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { self.tree.skip_finish() } - pub fn parse_with_module_docs(mut self) -> (ObjectTree, BTreeMap>) { + pub fn parse_with_module_docs( + mut self, + ) -> (ObjectTree, BTreeMap>) { self.tree.register_builtins(); self.run(); let docs = std::mem::take(&mut self.module_docs); @@ -406,25 +469,26 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { fn describe_parse_error(&mut self) -> DMError { let expected = self.expected.join(", "); if self.eof { - return self.error(format!("got EOF, expected one of: {}", expected)); - } - match self.next("") { - Ok(got) => { - let message = format!("got '{}', expected one of: {}", got, expected); - self.put_back(got); - let mut e = self.error(message); - if self.possible_indentation_error { - let mut loc = e.location(); - loc.line += 1; - loc.column = 1; - e.add_note(loc, "check for extra indentation at the start of the next line"); - self.possible_indentation_error = false; - } - e + let mut error = self.error(format!("got EOF, expected one of: {expected}")); + if let Some(loc) = self.skipping_location { + error.add_note(loc, "unmatched pair here"); } - Err(err) => self - .error(format!("i/o error, expected one of: {}", expected)) - .with_cause(err), + error + } else { + let got = self.peek(); + let message = format!("got '{got:#}', expected one of: {expected}"); + let mut error = self.error(message); + if self.possible_indentation_error { + let mut loc = error.location(); + loc.line += 1; + loc.column = 1; + error.add_note( + loc, + "check for extra indentation at the start of the next line", + ); + self.possible_indentation_error = false; + } + error } } @@ -440,58 +504,92 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } } - fn next>>(&mut self, expected: S) -> Result { - let tok = loop { - if let Some(next) = self.next.take() { - break Ok(next); + /// Push an alternative to the "got X, expected one of: ..." list. + fn expected(&mut self, expected: impl Into>) { + let expected = expected.into(); + if !expected.is_empty() && !self.expected.contains(&expected) { + self.expected.push(expected); + } + } + + /// Peek next token without consuming it, and update location. + fn peek(&mut self) -> &Token { + loop { + if let Some(ref next) = self.next { + break next; } match self.input.next() { Some(LocatedToken { location, - token: Token::DocComment(dc), - }) => match dc.target { - DocTarget::EnclosingItem if self.in_docs == 0 => { - self.module_docs - .entry(location.file) - .or_default() - .push((location.line, dc)); - } - DocTarget::EnclosingItem => self.docs_enclosing.push(dc), - DocTarget::FollowingItem => self.docs_following.push(dc), + token: Token::DocComment(comment), + }) => { + self.doc_comments_pending.push_back((location, comment)); + }, + Some(LocatedToken { location, token }) => { + self.location = location; + self.next = Some(token); }, - Some(token) => { - self.expected.clear(); - self.location = token.location; - break Ok(token.token); - } None => { if !self.eof { self.eof = true; - break Ok(Token::Eof); + self.next = Some(Token::Eof); } else { - break self.parse_error(); + panic!("internal parser error: kept parsing after EOF"); } - } + }, } - }; - let what = expected.into(); - if !what.is_empty() && !self.expected.contains(&what) { - self.expected.push(what); } - tok } - fn put_back(&mut self, tok: Token) { - if self.next.is_some() { - panic!("cannot put_back twice") + /// Consume next token unconditionally. Cannot be undone. Try `take_match!` instead. + fn take(&mut self) -> Token { + self.peek(); // Always populates self.next, so .take().unwrap() is OK + self.doc_comments_pending.clear(); + self.expected.clear(); + self.next.take().unwrap() + } + + fn next_comment_of_target(&mut self, target: DocTarget) -> Status { + loop { + if let Some((location, comment)) = self.doc_comments_pending.pop_front() { + if comment.target == target { + self.location = location; + return success(comment); + } else if comment.target == DocTarget::FollowingItem { + //^ out-of-position `//!` comments have to be dropped or they infect later declarations + self.doc_comments_pending.push_front((location, comment)); + return Ok(None); + } + } + if self.next.is_some() { + return Ok(None); + } + match self.input.next() { + Some(LocatedToken { + location, + token: Token::DocComment(comment), + }) => { + if comment.target == target { + self.location = location; + return success(comment); + } else if comment.target == DocTarget::FollowingItem { + //^ out-of-position `//!` comments have to be dropped or they infect later declarations + self.doc_comments_pending.push_front((location, comment)); + return Ok(None); + } + }, + Some(other) => { + self.location = other.location; + self.next = Some(other.token); + return Ok(None); + }, + None => return Ok(None), + } } - self.next = Some(tok); } fn updated_location(&mut self) -> Location { - if let Ok(token) = self.next("") { - self.put_back(token); - } + self.peek(); self.location } @@ -507,74 +605,53 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } } - fn try_another(&mut self, tok: Token) -> Status { - self.put_back(tok); - Ok(None) - } - fn exact(&mut self, tok: Token) -> Status<()> { - let message: Cow<'static, str> = match tok { - Token::Eof => "EOF".into(), - Token::Punct(p) => p.single_quoted().into(), - ref other => format!("'{}'", other).into(), - }; - let next = self.next(message)?; - if next == tok { + self.expected(tok.single_quoted()); + if self.peek() == &tok { + self.take(); SUCCESS } else { - self.try_another(next) + try_another() } } fn ident(&mut self) -> Status { - match self.next("identifier")? { - Token::Ident(i, _) => Ok(Some(i)), - other => self.try_another(other), - } - } - - fn dot(&mut self) -> Status<()> { - match self.next("'.'")? { - Token::Punct(Punctuation::Dot) => Ok(Some(())), - other => self.try_another(other), - } + self.expected("identifier"); + take_match!(self { + Token::Ident(i, _) => success(i), + } else try_another()) } fn ident_in_seq(&mut self, idx: usize) -> Status { let start = self.updated_location(); - match self.next("identifier")? { + take_match!(self { Token::Ident(i, _) => { self.annotate(start, || Annotation::InSequence(idx)); - Ok(Some(i)) + success(i) }, - other => self.try_another(other), - } + } else try_another()) } fn exact_ident(&mut self, ident: &'static str) -> Status<()> { - match self.next(ident)? { - Token::Ident(ref i, _) if i == ident => SUCCESS, - other => self.try_another(other), - } + self.expected(format!("'{ident}'")); + take_match!(self { + Token::Ident(i, _) if i == ident => SUCCESS, + } else try_another()) } // ------------------------------------------------------------------------ // Doc comment tracking - fn doc_comment Status>(&mut self, f: F) -> Status<(DocCollection, R)> { - use std::mem::replace; + fn following_doc_comment(&mut self) -> Status { + //self.expected("'///'"); + //self.expected("'/**'"); + self.next_comment_of_target(DocTarget::FollowingItem) + } - let enclosing = std::mem::take(&mut self.docs_enclosing); - let mut docs = std::mem::take(&mut self.docs_following); - self.in_docs += 1; - let result = f(self); - self.in_docs -= 1; - docs.extend(replace(&mut self.docs_enclosing, enclosing)); - match result { - Ok(Some(found)) => Ok(Some((docs, found))), - Ok(None) => Ok(None), - Err(err) => Err(err), - } + fn enclosing_doc_comment(&mut self) -> Status { + //self.expected("'//!'"); + //self.expected("'/*!'"); + self.next_comment_of_target(DocTarget::EnclosingItem) } // ------------------------------------------------------------------------ @@ -584,27 +661,39 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { self.tree_entries(self.tree.root_index(), None, None, Token::Eof) } - fn tree_block(&mut self, current: NodeIndex, proc_kind: Option, var_type: Option) -> Status<()> { - leading!(self.exact(Token::Punct(Punctuation::LBrace))); - Ok(Some(require!( - self.tree_entries(current, proc_kind, var_type, Token::Punct(Punctuation::RBrace)) - ))) - } - - fn tree_entries(&mut self, current: NodeIndex, proc_kind: Option, var_type: Option, terminator: Token) -> Status<()> { + fn tree_entries( + &mut self, + current: NodeIndex, + proc_builder: Option, + var_type: Option, + terminator: Token, + ) -> Status<()> { loop { - let message: Cow<'static, str> = match terminator { - Token::Eof => "newline".into(), - ref other => format!("newline, '{}'", other).into(), - }; - let next = self.next(message)?; - if next == terminator || next == Token::Eof { - break; - } else if next == Token::Punct(Punctuation::Semicolon) { - continue; + self.expected("';'"); + if terminator != Token::Eof { + self.expected(terminator.single_quoted()); } - self.put_back(next); - require!(self.tree_entry(current, proc_kind, var_type.clone())); + + if current == self.tree.root_index() { + self.expected("'//!'"); + self.expected("'/*!'"); + if let Some(module_comment) = self.enclosing_doc_comment()? { + // If we're not inside a type, this is where module `//!` comments appear. + self.module_docs + .entry(self.location.file) + .or_default() + .push((self.location.line, module_comment)); + continue; + } + } + + take_match!(self { + Token::Eof => break, + tok if tok == &terminator => break, + Token::Punct(Punctuation::Semicolon) => continue, + } else { + require!(self.tree_entry(current, proc_builder, var_type.clone())); + }); } SUCCESS } @@ -631,19 +720,24 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { self.annotate_precise(slash_loc..slash_loc, || { Annotation::IncompleteTreePath(absolute, parts.clone()) }); - self.context.register_error(self.error("path has no effect")); + self.context + .register_error(self.error("path has no effect")); return success((absolute, Vec::new())); - } + }, } // followed by ('/' ident)* - loop { - if self.slash()?.is_none() { - break; - } + while self.slash()?.is_some() { let mut slash_loc = self.location; if let Some(i) = self.ident_in_seq(parts.len())? { parts.push(i); } else { + // .../operator/ = ... / "operator/" + // but .../operator/ident = ... / "operator" / "ident" + let last = parts.last_mut().unwrap(); + if last == "operator" { + last.push('/'); + } + slash_loc.column += 1; self.annotate_precise(slash_loc..slash_loc, || { Annotation::IncompleteTreePath(absolute || always_absolute, parts.clone()) @@ -651,63 +745,85 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } } - self.annotate(start, || Annotation::TreePath(absolute || always_absolute, parts.clone())); + self.annotate(start, || { + Annotation::TreePath(absolute || always_absolute, parts.clone()) + }); success((absolute, parts)) } + /// Look for nothing, silently accept `/`, and complain but continue if we see a `.` or `:`. + /// + /// Return: `(absolute, spurious_lead)` fn possible_leading_slash(&mut self) -> Result<(bool, bool), DMError> { - match self.next("'/'")? { + self.expected("'/'"); + take_match!(self { Token::Punct(Punctuation::Slash) => Ok((true, false)), Token::Punct(p @ Punctuation::Dot) | Token::Punct(p @ Punctuation::CloseColon) | Token::Punct(p @ Punctuation::Colon) => { - self.error(format!("path started by '{}', should be unprefixed", p)) + self.error(format!("path started by '{p}', should be unprefixed")) .set_severity(Severity::Warning) .register(self.context); Ok((false, true)) - } - t => { - self.put_back(t); - Ok((false, false)) - } - } + }, + } else Ok((false, false))) } - // Look for a `/`, and complain but continue if we see a `.` or `:`. + /// Look for a `/`, and complain but continue if we see a `.` or `:`. fn slash(&mut self) -> Status<()> { - match self.next("'/'")? { + self.expected("'/'"); + take_match!(self { Token::Punct(Punctuation::Slash) => SUCCESS, Token::Punct(p @ Punctuation::Dot) | Token::Punct(p @ Punctuation::CloseColon) | Token::Punct(p @ Punctuation::Colon) => { - self.error(format!("path separated by '{}', should be '/'", p)) + self.error(format!("path separated by '{p}', should be '/'")) .set_severity(Severity::Warning) .register(self.context); SUCCESS - } - t => { self.put_back(t); Ok(None) } - } + }, + } else try_another()) } - fn tree_entry(&mut self, mut current: NodeIndex, mut proc_kind: Option, mut var_type: Option) -> Status<()> { + fn tree_entry( + &mut self, + mut current: NodeIndex, + mut proc_builder: Option, + mut var_type: Option, + ) -> Status<()> { // tree_entry :: path ';' - // tree_entry :: path tree_block + // tree_entry :: path '{' tree_entry* '}' // tree_entry :: path '=' expression ';' // tree_entry :: path '(' argument_list ')' ';' // tree_entry :: path '(' argument_list ')' code_block - use super::lexer::Token::*; use super::lexer::Punctuation::*; + use super::lexer::Token::*; let entry_start = self.updated_location(); + let mut docs = DocCollection::default(); + while let Some(doc_comment) = self.following_doc_comment()? { + docs.push(doc_comment); + } + // read and calculate the current path - let (absolute, mut path) = leading!(self.tree_path(false)); + let (absolute, mut path) = if docs.is_empty() { + leading!(self.tree_path(false)) + } else { + require!(self.tree_path(false)) + }; if absolute && current != self.tree.root_index() { - DMError::new(entry_start, format!("nested absolute path inside {}", self.tree.get_path(current))) - .set_severity(Severity::Warning) - .register(self.context); + DMError::new( + entry_start, + format!( + "nested absolute path inside {}", + self.tree.get_path(current) + ), + ) + .set_severity(Severity::Warning) + .register(self.context); current = self.tree.root_index(); } @@ -715,10 +831,10 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let (last_part, traverse) = match path.split_last_mut() { Some(x) => x, None => { - self.error("what?") + self.error("tree entry appears to have no name") .register(self.context); return SUCCESS; - } + }, }; let mut relative_type_location = None; @@ -734,19 +850,36 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { var_type.type_path.push(each.to_owned()); } } else if let Some(kind) = ProcDeclKind::from_name(each) { - proc_kind = Some(kind); - } else if proc_kind.is_some() { - self.error("cannot have sub-blocks of `proc/` block") - .register(self.context); + proc_builder = Some(ProcDeclBuilder::new(kind, None)); + } else if let Some(builder) = proc_builder.as_mut() { + let flags = ProcFlags::from_name(each); + if let Some(found) = flags { + builder.flags |= found + } else { + self.error("cannot have sub-blocks of `proc/` block") + .register(self.context); + } } else { - let len = self.tree.get_path(current).chars().filter(|&c| c == '/').count() + path_len; + let len = self + .tree + .get_path(current) + .chars() + .filter(|&c| c == '/') + .count() + + path_len; current = self.tree.subtype_or_add(self.location, current, each, len); - if !absolute && self.context.config().code_standards.disallow_relative_type_definitions { + if !absolute + && self + .context + .config() + .code_standards + .disallow_relative_type_definitions + { relative_type_location = Some(self.location); } } - } + }; } macro_rules! handle_relative_type_error { () => { @@ -755,7 +888,7 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { .set_severity(Severity::Warning) .register(self.context); } - } + }; } for each in traverse { @@ -764,7 +897,7 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // parse operator overloading definitions if last_part == "operator" { - self.try_read_operator_name(last_part)?; + let () = self.try_read_operator_name(last_part)?; } let var_suffix = if var_type.is_some() { @@ -773,66 +906,130 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { Default::default() }; + // Same-line `//!` comment for types. + // Also incidentally allows putting `/*!` in weird places, but that's probably fine. + while let Some(doc_comment) = self.enclosing_doc_comment()? { + docs.push(doc_comment); + } + // read the contents for real - match self.next("contents")? { - t @ Punct(LBrace) => { + self.expected("'='"); + self.expected("'('"); + self.expected("'{'"); + match self.peek() { + Punct(LBrace) => { + self.take(); // `thing{` - block - self.put_back(t); traverse_tree!(last_part); handle_relative_type_error!(); let start = self.updated_location(); - if proc_kind.is_some() || var_type.is_some() { - // Can't apply docs to `var/` or `proc/` blocks. - require!(self.tree_block(current, proc_kind, var_type.clone())); - } else { - let (comment, ()) = require!(self.doc_comment(|this| this.tree_block(current, proc_kind, var_type.clone()))); - self.tree.extend_docs(current, comment); + // Inside-block `//!` comments for types. + while let Some(comment) = self.enclosing_doc_comment()? { + docs.push(comment); } + if !docs.is_empty() && (proc_builder.is_some() || var_type.is_some()) { + // Can't apply docs to `var/` or `proc/` blocks. + DMError::new( + start, + "docs on `var/` or `proc/` block will be applied to their type", + ) + .set_severity(Severity::Warning) + .register(self.context); + } + self.tree.extend_docs(current, docs); + + require!(self.tree_entries( + current, + proc_builder, + var_type.clone(), + Token::Punct(Punctuation::RBrace) + )); let node = self.tree.get_path(current).to_owned(); - self.annotate(start, || Annotation::TreeBlock(reconstruct_path(&node, proc_kind, var_type.as_ref(), ""))); + self.annotate(start, || { + Annotation::TreeBlock(reconstruct_path( + &node, + proc_builder, + var_type.as_ref(), + "", + )) + }); SUCCESS - } + }, Punct(Assign) => { + self.take(); // `something=` - var handle_relative_type_error!(); let location = self.location; - // kind of goofy, but allows "enclosing" doc comments at the end of the line - // translators note: this allows comments of the form ``//! blah`` at the end of the line - let (docs, expression) = require!(self.doc_comment(|this| { - let expr = require!(this.expression()); - let _ = require!(this.input_specifier()); + let expression = require!(self.expression()); + // TODO: save `in` expression? + let (input_type, _) = require!(self.input_specifier()); - // We have to annotate prior to consuming the statement terminator, as we - // will otherwise consume following whitespace resulting in a bad annotation range - let node = this.tree.get_path(current).to_owned(); - this.annotate(entry_start, || Annotation::Variable(reconstruct_path(&node, proc_kind, var_type.as_ref(), last_part))); + // We have to annotate prior to consuming the statement terminator, as we + // will otherwise consume following whitespace resulting in a bad annotation range + let node = self.tree.get_path(current).to_owned(); + self.annotate(entry_start, || { + Annotation::Variable(reconstruct_path( + &node, + proc_builder, + var_type.as_ref(), + last_part, + )) + }); - require!(this.statement_terminator()); - success(expr) - })); + // Allow `//!` doc comments at the end of the line. + while let Some(comment) = self.enclosing_doc_comment()? { + docs.push(comment); + } + + require!(self.statement_terminator()); if let Some(mut var_type) = var_type { var_type.suffix(&var_suffix); - self.tree.declare_var(current, last_part, location, docs, var_type.build(), Some(expression)); + var_type.input_type = input_type; + self.tree.declare_var( + current, + last_part, + location, + docs, + var_type.build(), + Some(expression), + ); } else { - self.tree.override_var(current, last_part, location, docs, expression); + self.tree + .override_var(current, last_part, location, docs, expression); } SUCCESS - } - t @ Punct(LParen) => { + }, + Punct(LParen) => { // `something(` - proc - self.put_back(t); - require!(self.proc_params_and_body(current, proc_kind, last_part, entry_start, absolute)); + require!(self.proc_params_and_body( + current, + proc_builder, + last_part, + entry_start, + absolute, + docs + )); SUCCESS - } - other => { + }, + _ => { // usually `thing;` - a contentless declaration - // TODO: allow enclosing-targeting docs here somehow? - self.put_back(other); + + let input_type = if var_type.is_some() { + // TODO: save `in` expression? + require!(self.input_specifier()).0 + } else { + None + }; + + // Same-line `//!` comment AFTER + while let Some(comment) = self.enclosing_doc_comment()? { + docs.push(comment); + } if last_part == "var" { self.error("`var;` item has no effect") @@ -840,33 +1037,54 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { .register(self.context); } else if let Some(mut var_type) = var_type.take() { if VarTypeFlags::from_name(last_part).is_some() { - self.error(format!("`var/{};` item has no effect", last_part)) + self.error(format!("`var/{last_part};` item has no effect")) .set_severity(Severity::Warning) .register(self.context); } else { - let docs = std::mem::take(&mut self.docs_following); var_type.suffix(&var_suffix); + var_type.input_type = input_type; let node = self.tree.get_path(current).to_owned(); - self.annotate(entry_start, || Annotation::Variable(reconstruct_path(&node, proc_kind, Some(&var_type), last_part))); - self.tree.declare_var(current, last_part, self.location, docs, var_type.build(), var_suffix.into_initializer()); + self.annotate(entry_start, || { + Annotation::Variable(reconstruct_path( + &node, + proc_builder, + Some(&var_type), + last_part, + )) + }); + self.tree.declare_var( + current, + last_part, + self.location, + docs, + var_type.build(), + var_suffix.into_initializer(), + ); } } else if ProcDeclKind::from_name(last_part).is_some() { self.error("`proc;` item has no effect") .set_severity(Severity::Warning) .register(self.context); - } else if proc_kind.is_some() { + } else if proc_builder.is_some() { self.error("child of `proc/` without body") .register(self.context); } else { handle_relative_type_error!(); - let docs = std::mem::take(&mut self.docs_following); - let len = self.tree.get_path(current).chars().filter(|&c| c == '/').count() + path_len; - current = self.tree.subtype_or_add(self.location, current, last_part, len); + let len = self + .tree + .get_path(current) + .chars() + .filter(|&c| c == '/') + .count() + + path_len; + current = self + .tree + .subtype_or_add(self.location, current, last_part, len); self.tree.extend_docs(current, docs); } SUCCESS - } + }, } } @@ -876,14 +1094,18 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // ------------------------------------------------------------------------ // Object tree - Procs - fn try_read_operator_name(&mut self, last_part: &mut String) -> Status<()> { - use super::lexer::Token::Punct; + fn try_read_operator_name(&mut self, last_part: &mut String) -> Result<(), DMError> { use super::lexer::Punctuation::*; + use super::lexer::Token::Punct; if self.exact(Punct(Mod))?.is_some() { last_part.push('%'); } else if self.exact(Punct(ModAssign))?.is_some() { last_part.push_str("%="); + } else if self.exact(Punct(FloatMod))?.is_some() { + last_part.push_str("%%"); + } else if self.exact(Punct(FloatModAssign))?.is_some() { + last_part.push_str("%%="); } else if self.exact(Punct(BitAnd))?.is_some() { last_part.push('&'); } else if self.exact(Punct(BitAndAssign))?.is_some() { @@ -894,6 +1116,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { last_part.push_str("**"); } else if self.exact(Punct(MulAssign))?.is_some() { last_part.push_str("*="); + } else if self.exact(Punct(Slash))?.is_some() { + // Here for completeness, but REALLY handled in tree_path(). + last_part.push('/'); + } else if self.exact(Punct(DivAssign))?.is_some() { + last_part.push_str("/="); } else if self.exact(Punct(Add))?.is_some() { last_part.push('+'); } else if self.exact(Punct(PlusPlus))?.is_some() { @@ -914,6 +1141,8 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { last_part.push_str("<<="); } else if self.exact(Punct(LessEq))?.is_some() { last_part.push_str("<=") + } else if self.exact(Punct(LessOrGreater))?.is_some() { + last_part.push_str("<=>"); } else if self.exact(Punct(Greater))?.is_some() { last_part.push('>'); } else if self.exact(Punct(GreaterEq))?.is_some() { @@ -934,6 +1163,8 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { last_part.push('~'); } else if self.exact(Punct(Equiv))?.is_some() { last_part.push_str("~="); + } else if self.exact(Punct(AssignInto))?.is_some() { + last_part.push_str(":="); } else if self.exact(Punct(LBracket))?.is_some() { require!(self.exact(Punct(RBracket))); if self.exact(Punct(Assign))?.is_some() { @@ -941,47 +1172,70 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } else { last_part.push_str("[]"); } + } else if self.exact(Token::String("".to_string()))?.is_some() { + last_part.push_str("\"\"") } - SUCCESS + Ok(()) } - fn proc_params_and_body(&mut self, current: NodeIndex, proc_kind: Option, name: &str, entry_start: Location, absolute: bool) -> Status<()> { - use super::lexer::Token::*; + fn proc_params_and_body( + &mut self, + current: NodeIndex, + proc_builder: Option, + name: &str, + entry_start: Location, + absolute: bool, + mut docs: DocCollection, + ) -> Status<()> { use super::lexer::Punctuation::*; + use super::lexer::Token::*; leading!(self.exact(Punct(LParen))); let location = self.location; let parameters = require!(self.separated(Comma, RParen, None, Parser::proc_parameter)); + let return_type = self.return_type(proc_builder)?.unwrap_or_default(); // split off a subparser so we can keep parsing the objtree // even when the proc body doesn't parse - let mut body_start = self.location; let mut body_tt = Vec::new(); - // check that it doesn't end immediately (empty body) - let (comment, ()) = require!(self.doc_comment(|this| { - body_start = this.updated_location(); - if let Some(()) = this.statement_terminator()? { - body_tt.push(LocatedToken::new(this.location, Punct(Semicolon))); - } else { - // read an initial token tree - require!(this.read_any_tt(&mut body_tt)); - // if the first token is not an LBrace, it's on one line - if body_tt[0].token != Punct(LBrace) { - while this.statement_terminator()?.is_none() { - require!(this.read_any_tt(&mut body_tt)); - } - body_tt.push(LocatedToken::new(this.location, Punct(Semicolon))); - } + let body_start = self.updated_location(); + + // Accept `//!` comments right after the `)` of the proc. + while let Some(comment) = self.enclosing_doc_comment()? { + docs.push(comment); + } + + if let Some(()) = self.statement_terminator()? { + // proc has no body and just ends with `;` + body_tt.push(LocatedToken::new(self.location, Punct(Semicolon))); + } else if let Some(()) = self.exact(Punct(LBrace))? { + // proc has a body starting with `{` + body_tt.push(LocatedToken::new(self.location, Punct(LBrace))); + + // enclosing doc comments `//!` can appear after the `{` + while let Some(comment) = self.enclosing_doc_comment()? { + docs.push(comment); } - SUCCESS - })); + + // read the rest of the line + while self.exact(Punct(RBrace))?.is_none() { + require!(self.read_any_tt(&mut body_tt)); + } + body_tt.push(LocatedToken::new(self.location, Punct(RBrace))); + } else { + // proc has a same-line body, read until statement terminator + while self.statement_terminator()?.is_none() { + require!(self.read_any_tt(&mut body_tt)); + } + body_tt.push(LocatedToken::new(self.location, Punct(Semicolon))); + } let code = if self.procs { let result = { let mut subparser: Parser<'ctx, '_, '_> = Parser::new(self.context, body_tt); if let Some(a) = self.annotations.as_mut() { - subparser.annotations = Some(&mut *a); + subparser.annotations = Some(*a); } let block = subparser.block(&LoopContext::None); subparser.require(block) @@ -996,29 +1250,50 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { self.context.register_error(err); None }, - Ok(code) => { - Some(code) - } + Ok(code) => Some(code), } } else { None }; - match self.tree.register_proc(self.context, location, current, name, proc_kind, parameters, code) { + match self.tree.register_proc( + self.context, + location, + current, + name, + proc_builder, + parameters, + return_type, + code, + Some(body_start..self.location), + ) { Ok((idx, proc)) => { - proc.docs.extend(comment); + proc.docs.extend(docs); // manually performed for borrowck reasons if let Some(dest) = self.annotations.as_mut() { - let new_stack = reconstruct_path(&self.tree.get_path(current), proc_kind, None, name); - dest.insert(entry_start..body_start, Annotation::ProcHeader(new_stack.to_vec(), idx)); - dest.insert(body_start..self.location, Annotation::ProcBody(new_stack.to_vec(), idx)); + let new_stack = + reconstruct_path(self.tree.get_path(current), proc_builder, None, name); + dest.insert( + entry_start..body_start, + Annotation::ProcHeader(new_stack.to_vec(), idx), + ); + dest.insert( + body_start..self.location, + Annotation::ProcBody(new_stack.to_vec(), idx), + ); } - if !absolute && self.context.config().code_standards.disallow_relative_proc_definitions { + if !absolute + && self + .context + .config() + .code_standards + .disallow_relative_proc_definitions + { DMError::new(location, "relatively pathed proc defined here") .set_severity(Severity::Warning) .register(self.context); } - } + }, Err(e) => self.context.register_error(e), }; @@ -1026,14 +1301,14 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } fn proc_parameter(&mut self) -> Status { - use super::lexer::Token::*; use super::lexer::Punctuation::*; + use super::lexer::Token::*; if let Some(()) = self.exact(Punct(Ellipsis))? { return success(Parameter { name: "...".to_owned(), location: self.location, - .. Default::default() + ..Default::default() }); } @@ -1045,9 +1320,9 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { None => { self.describe_parse_error().register(self.context); "".to_owned() - } + }, }; - if path.first().map_or(false, |i| i == "var") { + if path.first().is_some_and(|i| i == "var") { path.remove(0); DMError::new(leading_loc, "'var/' is unnecessary here") .set_severity(Severity::Hint) @@ -1095,8 +1370,8 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { /// Parse list size declarations. fn var_suffix(&mut self) -> Status { - use super::lexer::Token::Punct; use super::lexer::Punctuation::*; + use super::lexer::Token::Punct; let mut list = Vec::new(); while let Some(()) = self.exact(Punct(LBracket))? { @@ -1132,6 +1407,32 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { success((input_type, in_list)) } + /// Parse an optional as return type signifier (for procs) + fn return_type(&mut self, proc_builder: Option) -> Status { + if self.exact_ident("as")?.is_none() { + return try_another(); + } + + if proc_builder.is_none() { + self.error("cannot specify a return type for a proc override") + .register(self.context); + } + + // Alternative one: a traditional input type as usually follows `as`. + if let Some(input_type) = self.input_type()? { + return success(ProcReturnType::InputType(input_type)); + } + + // Option two: a typepath. + require!(self.exact(Token::Punct(Punctuation::Slash))); + let mut path_vec = vec![require!(self.ident())]; + while let Some(()) = self.exact(Token::Punct(Punctuation::Slash))? { + path_vec.push(require!(self.ident())); + } + + success(ProcReturnType::TypePath(path_vec)) + } + /// Parse a verb input type. Used by proc params and the input() form. fn input_type(&mut self) -> Status { // Not supporting `as((mob|obj)|turf)` constructs right now. @@ -1142,19 +1443,21 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let ident = leading!(self.ident()); let mut as_what = match InputType::from_str(&ident) { - Some(what) => what, - None => { - self.context.register_error(self.error(format!("bad input type: '{}'", ident))); + Ok(what) => what, + Err(()) => { + self.context + .register_error(self.error(format!("bad input type: '{ident}'"))); InputType::empty() - } + }, }; while let Some(()) = self.exact(Token::Punct(Punctuation::BitOr))? { let ident = require!(self.ident()); match InputType::from_str(&ident) { - Some(what) => as_what |= what, - None => { - self.context.register_error(self.error(format!("bad input type: '{}'", ident))); - } + Ok(what) => as_what |= what, + Err(()) => { + self.context + .register_error(self.error(format!("bad input type: '{ident}'"))); + }, } } success(as_what) @@ -1189,7 +1492,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { success(result.into_boxed_slice()) } - fn statement(&mut self, loop_ctx: &LoopContext, vars: &mut Vec<(Location, VarType, Ident)>) -> Status> { + fn statement( + &mut self, + loop_ctx: &LoopContext, + vars: &mut Vec<(Location, VarType, Ident)>, + ) -> Status> { let start = self.location(); let spanned = |v| success(Spanned::new(start, v)); @@ -1235,7 +1542,10 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let condition = Spanned::new(self.location(), require!(self.expression())); require!(self.exact(Token::Punct(Punctuation::RParen))); require!(self.statement_terminator()); - spanned(Statement::DoWhile { block, condition: Box::new(condition) }) + spanned(Statement::DoWhile { + block, + condition: Box::new(condition), + }) } else if let Some(()) = self.exact_ident("for")? { // for () // for (Var [as Type] [in List]) Statement @@ -1244,10 +1554,19 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // for (Var = Low to High) require!(self.exact(Token::Punct(Punctuation::LParen))); let init = self.simple_statement(true, vars)?; - if let Some(()) = self.comma_or_semicolon()? { - // three-pronged loop form ("for loop") + + // We do this here otherwise it's consumed after the comma (for key-value loops with typed keys) + let key_input_type = if let Some(()) = self.exact_ident("as")? { + Some(require!(self.input_type())) + } else { + None + }; + + // three-pronged loop form ("for loop") + if let Some(()) = self.semicolon()? { + // for(init; test; [inc]) let test = self.expression()?; - let inc = match self.comma_or_semicolon()? { + let inc = match self.semicolon()? { Some(()) => self.simple_statement(false, vars)?, None => None, }; @@ -1258,6 +1577,77 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { inc: inc.map(Box::new), block: require!(self.block(&LoopContext::ForLoop)), }) + // ... copypasted but with commas + } else if let Some(()) = self.comma()? { + // for(init, test [, inc]) + let test = self.expression()?; + let inc = match self.comma()? { + Some(()) => self.simple_statement(false, vars)?, + None => None, + }; + // Increment exists + if inc.is_some() { + require!(self.exact(Token::Punct(Punctuation::RParen))); + spanned(Statement::ForLoop { + init: init.map(Box::new), + test: test.map(Box::new), + inc: inc.map(Box::new), + block: require!(self.block(&LoopContext::ForLoop)), + }) + } else { + // for (..., ... in ...) + match test { + // This should necessarily be caught because the expression is going be + //for(var/k, [v in x]) and [v in x] will be passed as BinaryOp::In + // This is a bit ugly but it workss + Some(Expression::BinaryOp { + op: BinaryOp::In, + lhs, + rhs, + }) => { + // First thing first: for (var/k, v in x) REQUIRES the var/k. + // This unboxes it and rejects other type for(x, [...]), for(0, [...]) + let (var_type, key) = match init { + // this is a really terrible way to do this + Some(Statement::Var(var_statement)) => match var_statement.value { + None => (Some(var_statement.var_type), var_statement.name), + _ => return Err(self.error("cannot assigned a value to var/key in a for(var/key, value) statement")), + }, + _ => return Err(self.error("for (var/key, value) requires a 'var' keyword")), + }; + // Value is the lhs of for(var/k, [v in x]) + // It should also pass only if it's an ident + let value = match lhs.into_term() { + Some(Term::Ident(value)) => value, + _ => return Err(self.error( + "value must be a variable in a for (var/key, value) statement", + )), + }; + // TODO : check if `x` is an ident/a "list()" or "alist()" statement ? + require!(self.exact(Token::Punct(Punctuation::RParen))); + // Returns a for(k,v) + spanned(Statement::ForKeyValue(Box::new(ForKeyValueStatement { + var_type: Some(var_type.expect("/")), + key: key.into(), + key_input_type, + value: value.into(), + in_list: Some(*rhs), // We'll assume the rhs of [v in x] is a list. Any other case, DM will catch anyway. + block: require!(self.block(&LoopContext::ForLoop)), + }))) + }, + // We will just assume everything else for(k, [...]) is a two-pronged for loop + // for (init, test) {...} + _ => { + require!(self.exact(Token::Punct(Punctuation::RParen))); + spanned(Statement::ForLoop { + init: init.map(Box::new), + test: test.map(Box::new), + inc: inc.map(Box::new), + block: require!(self.block(&LoopContext::ForLoop)), + }) + }, + } + } } else if let Some(init) = init { // in-list form ("for list") let (var_type, name) = match init { @@ -1268,8 +1658,13 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // for(var/a = 1 to require!(self.exact_ident("to")); let rhs = require!(self.expression()); - return spanned(require!(self.for_range(Some(vs.var_type), vs.name, Box::new(value), Box::new(rhs)))); - } + return spanned(require!(self.for_range( + Some(vs.var_type), + vs.name, + value, + rhs + ))); + }, }, Statement::Expr(Expression::AssignOp { op: AssignOp::Assign, @@ -1283,8 +1678,8 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { }; require!(self.exact_ident("to")); let to_rhs = require!(self.expression()); - return spanned(require!(self.for_range(None, name, rhs, Box::new(to_rhs)))); - } + return spanned(require!(self.for_range(None, name, *rhs, to_rhs))); + }, Statement::Expr(Expression::BinaryOp { op: BinaryOp::In, lhs, @@ -1294,14 +1689,13 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { Some(Term::Ident(name)) => name, _ => return Err(self.error("for-list must start with variable")), }; - // Explicit move is necessary because rustc becomes - // confused when matching on the *rhs lvalue, thinking - // moving the LHS also moves the RHS. This fails: - // let a: Box<(NonCopy, NonCopy)>; - // let (b, c) = *a; - match {*rhs} { - Expression::BinaryOp { op: BinaryOp::To, lhs, rhs } => { - return spanned(require!(self.for_range(None, name, lhs, rhs))); + match *rhs { + Expression::BinaryOp { + op: BinaryOp::To, + lhs, + rhs, + } => { + return spanned(require!(self.for_range(None, name, *lhs, *rhs))); }, rhs => { // I love code duplication, don't you? @@ -1313,7 +1707,7 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { in_list: Some(rhs), block: require!(self.block(&LoopContext::ForList)), }))); - } + }, } }, Statement::Expr(expr) => match expr.into_term() { @@ -1334,7 +1728,7 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let value = require!(self.expression()); if let Some(()) = self.exact_ident("to")? { let rhs = require!(self.expression()); - return spanned(require!(self.for_range(var_type, name, Box::new(value), Box::new(rhs)))); + return spanned(require!(self.for_range(var_type, name, value, rhs))); } Some(value) } else { @@ -1365,7 +1759,7 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } spanned(Statement::Spawn { delay: expr, - block: require!(self.block(&LoopContext::None)) + block: require!(self.block(&LoopContext::None)), }) } else if let Some(()) = self.exact_ident("switch")? { require!(self.exact(Token::Punct(Punctuation::LParen))); @@ -1373,19 +1767,29 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { require!(self.exact(Token::Punct(Punctuation::RParen))); require!(self.exact(Token::Punct(Punctuation::LBrace))); let mut cases = Vec::new(); - while let Some(()) = self.exact_ident("if")? { - require!(self.exact(Token::Punct(Punctuation::LParen))); - let what = require!(self.separated(Punctuation::Comma, Punctuation::RParen, None, Parser::case)); - if what.is_empty() { - self.context.register_error(self.error("switch case cannot be empty")); + let default = loop { + if let Some(()) = self.exact_ident("if")? { + require!(self.exact(Token::Punct(Punctuation::LParen))); + let what = require!(self.separated( + Punctuation::Comma, + Punctuation::RParen, + None, + Parser::case + )); + if what.is_empty() { + self.context + .register_error(self.error("switch case cannot be empty")); + } + let block = require!(self.block(loop_ctx)); + cases.push((Spanned::new(self.location(), what), block)); + } else if let Some(()) = self.exact_ident("else")? { + break Some(require!(self.block(loop_ctx))); + } else if let Some(()) = self.exact(Token::Punct(Punctuation::Semicolon))? { + // Tolerate stray semicolons here because inert doc + // comments might synthesize them. + } else { + break None; } - let block = require!(self.block(loop_ctx)); - cases.push((Spanned::new(self.location(), what), block)); - } - let default = if let Some(()) = self.exact_ident("else")? { - Some(require!(self.block(loop_ctx))) - } else { - None }; require!(self.exact(Token::Punct(Punctuation::RBrace))); spanned(Statement::Switch { @@ -1397,15 +1801,16 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let try_block = require!(self.block(loop_ctx)); self.skip_phantom_semicolons()?; require!(self.exact_ident("catch")); - let catch_params; - if let Some(()) = self.exact(Token::Punct(Punctuation::LParen))? { - catch_params = require!(self.separated(Punctuation::Comma, Punctuation::RParen, None, |this| { - // TODO: improve upon this cheap approximation - success(leading!(this.tree_path(true)).1.into_boxed_slice()) - })); + let catch_params = if let Some(()) = self.exact(Token::Punct(Punctuation::LParen))? { + require!( + self.separated(Punctuation::Comma, Punctuation::RParen, None, |this| { + // TODO: improve upon this cheap approximation + success(leading!(this.tree_path(true)).1.into_boxed_slice()) + }) + ) } else { - catch_params = Vec::new(); - } + Vec::new() + }; let catch_block = require!(self.block(loop_ctx)); spanned(Statement::TryCatch { try_block, @@ -1424,7 +1829,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { }; let value = require!(self.expression()); require!(self.statement_terminator()); - spanned(Statement::Setting { name: name.into(), mode, value }) + spanned(Statement::Setting { + name: name.into(), + mode, + value, + }) } else if let Some(()) = self.exact_ident("break")? { let label = self.ident()?; require!(self.statement_terminator()); @@ -1458,21 +1867,20 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } } - // Handle if(1){a=1;b=2} without a trailing semicolon fn statement_terminator(&mut self) -> Status<()> { - match self.next("';'")? { - Token::Punct(Punctuation::Semicolon) => SUCCESS, - p @ Token::Punct(Punctuation::RBrace) => { - self.put_back(p); + self.expected("';'"); + match self.peek() { + Token::Punct(Punctuation::Semicolon) => { + self.take(); SUCCESS - } - p @ Token::Punct(Punctuation::LBrace) => { + }, + // Handle if(1){a=1;b=2} without a trailing semicolon + Token::Punct(Punctuation::RBrace) => SUCCESS, + Token::Punct(Punctuation::LBrace) => { self.possible_indentation_error = true; - self.try_another(p) - } - other => { - self.try_another(other) - } + try_another() + }, + _ => try_another(), } } @@ -1490,7 +1898,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } // Single-line statements. Can appear in for loops. Followed by a semicolon. - fn simple_statement(&mut self, in_for: bool, vars: &mut Vec<(Location, VarType, Ident)>) -> Status { + fn simple_statement( + &mut self, + in_for: bool, + vars: &mut Vec<(Location, VarType, Ident)>, + ) -> Status { if let Some(()) = self.exact_ident("var")? { // statement :: 'var' type_path name ('=' value) let mut var_stmts = Vec::new(); @@ -1516,16 +1928,22 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { .register(self.context); } if var_type.flags.is_private() { - DMError::new(type_path_start, "var/SpacemanDMM_private has no effect here") - .with_errortype("private_var") - .set_severity(Severity::Warning) - .register(self.context); + DMError::new( + type_path_start, + "var/SpacemanDMM_private has no effect here", + ) + .with_errortype("private_var") + .set_severity(Severity::Warning) + .register(self.context); } if var_type.flags.is_protected() { - DMError::new(type_path_start, "var/SpacemanDMM_protected has no effect here") - .with_errortype("protected_var") - .set_severity(Severity::Warning) - .register(self.context); + DMError::new( + type_path_start, + "var/SpacemanDMM_protected has no effect here", + ) + .with_errortype("protected_var") + .set_severity(Severity::Warning) + .register(self.context); } let var_suffix = require!(self.var_suffix()); var_type.suffix(&var_suffix); @@ -1551,7 +1969,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { .register(self.context); } - var_stmts.push(VarStatement { var_type: var_type.build(), name, value }); + var_stmts.push(VarStatement { + var_type: var_type.build(), + name, + value, + }); if in_for || self.exact(Token::Punct(Punctuation::Comma))?.is_none() { break; } @@ -1591,12 +2013,13 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // for(var/a = 1 to 20 // for(var/a in 1 to 20 + // This... isn't a boxed local, it's method arguments. Clippy?? fn for_range( &mut self, var_type: Option, name: Ident, - start: Box, - end: Box, + start: Expression, + end: Expression, ) -> Status { // step 2 let step = if let Some(()) = self.exact_ident("step")? { @@ -1610,17 +2033,23 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { success(Statement::ForRange(Box::new(ForRangeStatement { var_type, name: name.into(), - start: *start, - end: *end, + start, + end, step, block: require!(self.block(&LoopContext::ForRange)), }))) } - fn comma_or_semicolon(&mut self) -> Status<()> { + fn comma(&mut self) -> Status<()> { if let Some(()) = self.exact(Token::Punct(Punctuation::Comma))? { SUCCESS - } else if let Some(()) = self.exact(Token::Punct(Punctuation::Semicolon))? { + } else { + Ok(None) + } + } + + fn semicolon(&mut self) -> Status<()> { + if let Some(()) = self.exact(Token::Punct(Punctuation::Semicolon))? { SUCCESS } else { Ok(None) @@ -1640,12 +2069,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // Expressions fn path_separator(&mut self) -> Status { - success(match self.next("path separator")? { - Token::Punct(Punctuation::Slash) => PathOp::Slash, - Token::Punct(Punctuation::Dot) => PathOp::Dot, - Token::Punct(Punctuation::CloseColon) => PathOp::Colon, - other => return self.try_another(other), - }) + take_match!(self { + Token::Punct(Punctuation::Slash) => success(PathOp::Slash), + Token::Punct(Punctuation::Dot) => success(PathOp::Dot), + Token::Punct(Punctuation::CloseColon) => success(PathOp::Colon), + } else try_another()) } // distinct from a tree_path, path must begin with a path separator and can @@ -1698,51 +2126,72 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // parse vars if we find them let mut vars = Vec::new(); if let Some(()) = self.exact(Token::Punct(Punctuation::LBrace))? { - self.separated(Punctuation::Semicolon, Punctuation::RBrace, Some(()), |this| { - let key = require!(this.ident()); - require!(this.exact(Token::Punct(Punctuation::Assign))); - let value = require!(this.expression()); - vars.push((key.into(), value)); - SUCCESS - })?; + self.separated( + Punctuation::Semicolon, + Punctuation::RBrace, + Some(()), + |this| { + let key = require!(this.ident()); + require!(this.exact(Token::Punct(Punctuation::Assign))); + let value = require!(this.expression()); + vars.push((key.into(), value)); + SUCCESS + }, + )?; } - success(Box::new(Prefab { path: parts, vars: vars.into_boxed_slice() })) + success(Box::new(Prefab { + path: parts, + vars: vars.into_boxed_slice(), + })) } fn expression(&mut self) -> Status { self.expression_ex(None, false) } - fn expression_ex(&mut self, strength: Option, in_ternary: bool) -> Status { + fn expression_ex( + &mut self, + strength: Option, + in_ternary: bool, + ) -> Status { let mut expr = leading!(self.group(in_ternary)); loop { // try to read the next operator - let next = self.next("operator")?; - let &info = match BINARY_OPS.iter().find(|op| op.matches(&next)) { - Some(info) => info, - None => { - self.put_back(next); - break; - } + self.expected("operator"); + let peek = self.peek(); + let Some(&info) = BINARY_OPS.iter().find(|op| op.matches(peek)) else { + break; }; // If we're a sub-expression within a ternary expression, don't try to read further than our parent's precedence would allow if let Some(strength) = strength { if info.strength > strength { - self.put_back(Token::Punct(info.token)); break; } } + self.take(); + // trampoline high-strength expression parts as the lhs of the newly found op - expr = require!(self.expression_part(expr, info, strength, - in_ternary || info.strength == Strength::Conditional)); + expr = require!(self.expression_part( + expr, + info, + strength, + in_ternary || info.strength == Strength::Conditional + )); } success(expr) } - fn expression_part(&mut self, lhs: Expression, prev_op: OpInfo, strength: Option, in_ternary: bool) -> Status { + #[allow(clippy::only_used_in_recursion)] + fn expression_part( + &mut self, + lhs: Expression, + prev_op: OpInfo, + strength: Option, + in_ternary: bool, + ) -> Status { use std::cmp::Ordering; let mut bits = vec![lhs]; @@ -1750,33 +2199,35 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let mut rhs = require!(self.group(in_ternary)); loop { // try to read the next operator... - let next = self.next("operator")?; - let &info = match BINARY_OPS.iter().find(|op| op.matches(&next)) { - Some(info) => info, - None => { - self.put_back(next); - break; - } + self.expected("operator"); + let peek = self.peek(); + let Some(&info) = BINARY_OPS.iter().find(|op| op.matches(peek)) else { + break; }; // Strength is in reverse order: A < B means A binds tighter match info.strength.cmp(&prev_op.strength) { Ordering::Less => { // the operator is stronger than us... recurse down - rhs = require!(self.expression_part(rhs, info, strength, - in_ternary || info.strength == Strength::Conditional)); - } + self.take(); + rhs = require!(self.expression_part( + rhs, + info, + strength, + in_ternary || info.strength == Strength::Conditional + )); + }, Ordering::Greater => { // the operator is weaker than us... return up - self.put_back(Token::Punct(info.token)); break; - } + }, Ordering::Equal => { // the same strength... push it to the list + self.take(); ops.push(info.oper); bits.push(rhs); rhs = require!(self.group(in_ternary)); - } + }, } } @@ -1797,14 +2248,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let mut result = rhs; while let Some(lhs) = bits.pop() { // Ensure that the next thing we see is a ':' by now. - match self.next("':'")? { + self.expected("':'"); + take_match!(self { Token::Punct(Punctuation::Colon) | - Token::Punct(Punctuation::CloseColon) => {} - other => { - self.put_back(other); - return self.parse_error() - } - } + Token::Punct(Punctuation::CloseColon) => {}, + } else return self.parse_error()); // Read the else branch. let else_ = match self.expression_ex(Some(Strength::Conditional), true)? { Some(else_) => else_, @@ -1813,7 +2261,7 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { .set_severity(Severity::Warning) .register(self.context); Expression::from(Term::Null) - } + }, }; // Compose the result. result = Expression::TernaryOp { @@ -1839,7 +2287,10 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { for (item, op) in iter.zip(&mut ops_iter) { result = op.build(Box::new(result), Box::new(item)); } - ops_iter.next().unwrap().build(Box::new(result), Box::new(rhs)) + ops_iter + .next() + .unwrap() + .build(Box::new(result), Box::new(rhs)) }) } @@ -1848,17 +2299,16 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // Read prefix unary ops let mut unary_ops = Vec::new(); loop { - match self.next("operator")? { + self.expected("operator"); + take_match!(self { Token::Punct(Punctuation::Sub) => unary_ops.push(Spanned::new(self.location, Follow::Unary(UnaryOp::Neg))), Token::Punct(Punctuation::Not) => unary_ops.push(Spanned::new(self.location, Follow::Unary(UnaryOp::Not))), Token::Punct(Punctuation::BitNot) => unary_ops.push(Spanned::new(self.location, Follow::Unary(UnaryOp::BitNot))), Token::Punct(Punctuation::PlusPlus) => unary_ops.push(Spanned::new(self.location, Follow::Unary(UnaryOp::PreIncr))), Token::Punct(Punctuation::MinusMinus) => unary_ops.push(Spanned::new(self.location, Follow::Unary(UnaryOp::PreDecr))), - other => { - self.put_back(other); - break; - } - } + Token::Punct(Punctuation::BitAnd) => unary_ops.push(Spanned::new(self.location, Follow::Unary(UnaryOp::Reference))), + Token::Punct(Punctuation::Mul) => unary_ops.push(Spanned::new(self.location, Follow::Unary(UnaryOp::Dereference))), + } else break); } let mut belongs_to = Vec::new(); @@ -1871,17 +2321,16 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // Read postfix unary ops and field-access follows let mut follow = Vec::new(); loop { - match self.next("operator")? { + self.expected("operator"); + take_match!(self { Token::Punct(Punctuation::PlusPlus) => follow.push(Spanned::new(self.location, Follow::Unary(UnaryOp::PostIncr))), Token::Punct(Punctuation::MinusMinus) => follow.push(Spanned::new(self.location, Follow::Unary(UnaryOp::PostDecr))), - other => { - self.put_back(other); - match self.follow(&mut belongs_to, in_ternary)? { - Some(f) => follow.push(f), - None => break, - } + } else { + match self.follow(&mut belongs_to, in_ternary)? { + Some(f) => follow.push(f), + None => break, } - } + }); } // Add prefix unary operators to the follows in reverse order @@ -1905,7 +2354,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { use super::lexer::Punctuation::*; let start = self.updated_location(); - let term = match self.next("term")? { + // We look for a lot of different words here, so just explain the categories. + self.expected("literal"); + self.expected("variable"); + self.expected("proc call"); + let term = take_match!(self { // term :: 'new' (prefab | (ident field*))? arglist? Token::Ident(ref i, _) if i == "new" => { // It's not entirely clear what is supposed to be valid here. @@ -1922,7 +2375,7 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // The following is what seems a reasonable approximation. // Try to read an ident or path, then read the arguments. - if self.dot()?.is_some() { + if let Some(()) = self.exact(Token::Punct(Punctuation::Dot))? { if let Some(ident) = self.ident()? { // prefab // TODO: arrange for this ident to end up in the prefab's annotation @@ -1974,12 +2427,45 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { None => Term::Ident(i.to_owned()), }, + Token::Ident(ref i, _) if i == "alist" => match self.arguments(&[], "alist")? { + Some(args) => Term::List(args), + None => Term::Ident(i.to_owned()), + }, + // term :: 'call' arglist arglist Token::Ident(ref i, _) if i == "call" => Term::DynamicCall( require!(self.arguments(&[], "call")), require!(self.arguments(&[], "call*")), ), + // term :: 'call_ext' ([library,] function) arglist + Token::Ident(ref i, _) if i == "call_ext" => { + require!(self.exact(Token::Punct(Punctuation::LParen))); + let first = require!(self.expression()); + let second = if self.exact(Token::Punct(Punctuation::Comma))?.is_some() { + Some(require!(self.expression())) + } else { + None + }; + require!(self.exact(Token::Punct(Punctuation::RParen))); + + let args = require!(self.arguments(&[], "call_ext*")); + match second { + // call_ext(library, function)(...) + Some(function) => Term::ExternalCall { + library: Some(Box::new(first)), + function: Box::new(function), + args, + }, + // call_ext(loaded_func)(...) + None => Term::ExternalCall { + library: None, + function: Box::new(first), + args, + } + } + }, + // term :: 'input' arglist input_specifier Token::Ident(ref i, _) if i == "input" => match self.arguments(&[], "input")? { Some(args) => { @@ -1997,7 +2483,7 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { Token::Ident(ref i, _) if i == "locate" => match self.arguments(&[], "locate")? { Some(args) => { // warn against this mistake - if let Some(&Expression::BinaryOp { op: BinaryOp::In, .. } ) = args.get(0) { + if let Some(&Expression::BinaryOp { op: BinaryOp::In, .. } ) = args.first() { self.error("bad `locate(X in Y)`, should be `locate(X) in Y`") .set_severity(Severity::Warning) .register(self.context); @@ -2034,6 +2520,23 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { require!(self.exact(Token::Punct(Punctuation::RParen))); Term::As(input_type) }, + // term :: __PROC__ + Token::Ident(ref i, _) if i == "__PROC__" => { + // We cannot replace with the proc path yet, you don't need one it's fine + Term::__PROC__ + }, + + // term :: __TYPE__ + Token::Ident(ref i, _) if i == "__TYPE__" => { + // We cannot replace with the typepath yet, so we'll hand back a term we can parse later + Term::__TYPE__ + }, + + // term :: __IMPLIED_TYPE__ + Token::Ident(ref i, _) if i == "__IMPLIED_TYPE__" => { + // We cannot replace with the typepath yet, so we'll hand back a term we can parse later + Term::__IMPLIED_TYPE__ + }, // term :: ident arglist | ident Token::Ident(i, _) => { @@ -2077,11 +2580,17 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { Term::Ident(".".to_owned()) } }, - // term :: path_lit - t @ Token::Punct(Punctuation::Slash) | - t @ Token::Punct(Punctuation::CloseColon) => { - self.put_back(t); - Term::Prefab(require!(self.prefab())) + Token::Punct(Punctuation::Scope) => { + if let Some(ident) = self.ident()? { + if let Some(args) = self.arguments(&[], "::")? { + Term::GlobalCall(Ident2::from(ident), args) + } else { + Term::GlobalIdent(Ident2::from(ident)) + } + } else { + // Go away + return self.parse_error() + } }, // term :: str_lit | num_lit @@ -2111,7 +2620,8 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let mut parts = Vec::new(); loop { let expr = self.expression()?; - match self.next("']'")? { + self.expected("']'"); + take_match!(self { Token::InterpStringPart(part) => { parts.push((expr, part.into())); }, @@ -2119,14 +2629,18 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { parts.push((expr, end.into())); break; }, - _ => return self.parse_error(), - } + } else return self.parse_error()); } Term::InterpString(begin.into(), parts.into()) }, - - other => return self.try_another(other), - }; + } else match self.peek() { + // term :: prefab + Token::Punct(Punctuation::Slash) | + Token::Punct(Punctuation::CloseColon) => { + Term::Prefab(require!(self.prefab())) + }, + _ => return try_another(), + }); success(Spanned::new(start, term)) } @@ -2134,16 +2648,19 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { let first_location = self.updated_location(); // follow :: ('[' | '?[') expression ']' - let kind = match self.next("field access")? { + self.expected("field access"); + let kind = take_match!(self { Token::Punct(Punctuation::LBracket) => ListAccessKind::Normal, Token::Punct(Punctuation::SafeLBracket) => ListAccessKind::Safe, - other => return self.try_another(other), - }; + } else return try_another()); belongs_to.clear(); let expr = require!(self.expression()); require!(self.exact(Token::Punct(Punctuation::RBracket))); - success(Spanned::new(first_location, Follow::Index(kind, Box::new(expr)))) + success(Spanned::new( + first_location, + Follow::Index(kind, Box::new(expr)), + )) } fn follow(&mut self, belongs_to: &mut Vec, in_ternary: bool) -> Status> { @@ -2154,15 +2671,15 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } // follow :: '.' ident arglist? - let kind = match self.next("field access")? { + self.expected("field access"); + let kind = take_match!(self { // TODO: only apply these rules if there is no whitespace around the punctuation Token::Punct(Punctuation::Dot) => PropertyAccessKind::Dot, Token::Punct(Punctuation::CloseColon) if !belongs_to.is_empty() || !in_ternary => PropertyAccessKind::Colon, Token::Punct(Punctuation::SafeDot) => PropertyAccessKind::SafeDot, Token::Punct(Punctuation::SafeColon) => PropertyAccessKind::SafeColon, - - other => return self.try_another(other), - }; + Token::Punct(Punctuation::Scope) => PropertyAccessKind::Scope, + } else return try_another()); let mut index_op_loc = self.location; let start = self.updated_location(); @@ -2176,24 +2693,34 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // register the parse error, but keep going self.context.register_error(self.describe_parse_error()); String::new() - } + }, }; let end = self.updated_location(); let follow = match self.arguments(belongs_to, &ident)? { Some(args) => { if !belongs_to.is_empty() { - let past = std::mem::replace(belongs_to, Vec::new()); - self.annotate_precise(start..end, || Annotation::ScopedCall(past, ident.clone())); + let past = std::mem::take(belongs_to); + self.annotate_precise(start..end, || { + Annotation::ScopedCall(past, ident.clone()) + }); + } + match kind { + PropertyAccessKind::Scope => Follow::ProcReference(ident.into()), + _ => Follow::Call(kind, ident.into(), args), } - Follow::Call(kind, ident.into(), args.into()) }, None => { if !belongs_to.is_empty() { - self.annotate_precise(start..end, || Annotation::ScopedVar(belongs_to.clone(), ident.clone())); + self.annotate_precise(start..end, || { + Annotation::ScopedVar(belongs_to.clone(), ident.clone()) + }); belongs_to.push(ident.clone()); } - Follow::Field(kind, ident.into()) + match kind { + PropertyAccessKind::Scope => Follow::StaticField(ident.into()), + _ => Follow::Field(kind, ident.into()), + } }, }; success(Spanned::new(first_location, follow)) @@ -2202,16 +2729,15 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // TODO: somehow fix the fact that this is basically copy-pasted from // follow() above. fn field(&mut self, belongs_to: &mut Vec, in_ternary: bool) -> Status { - let kind = match self.next("field access")? { + self.expected("field access"); + let kind = take_match!(self { // follow :: '.' ident // TODO: only apply these rules if there is no whitespace around the punctuation Token::Punct(Punctuation::Dot) => PropertyAccessKind::Dot, Token::Punct(Punctuation::CloseColon) if !belongs_to.is_empty() || !in_ternary => PropertyAccessKind::Colon, Token::Punct(Punctuation::SafeDot) => PropertyAccessKind::SafeDot, Token::Punct(Punctuation::SafeColon) => PropertyAccessKind::SafeColon, - - other => return self.try_another(other), - }; + } else return try_another()); let mut index_op_loc = self.location; let start = self.updated_location(); @@ -2225,15 +2751,20 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // register the parse error, but keep going self.context.register_error(self.describe_parse_error()); String::new() - } + }, }; let end = self.updated_location(); if !belongs_to.is_empty() { - self.annotate_precise(start..end, || Annotation::ScopedVar(belongs_to.clone(), ident.clone())); + self.annotate_precise(start..end, || { + Annotation::ScopedVar(belongs_to.clone(), ident.clone()) + }); belongs_to.push(ident.clone()); } - success(Field { kind, ident: ident.into() }) + success(Field { + kind, + ident: ident.into(), + }) } /// a parenthesized, comma-separated list of expressions @@ -2251,12 +2782,12 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { Ok(Some(expr)) => { arguments.push(expr); SUCCESS - } + }, Ok(None) => Ok(None), Err(e) => Err(e), } }); - let end = self.location; // location of the closing parenthesis + let end = self.location; // location of the closing parenthesis self.annotate_precise(start..end, || { Annotation::ProcArguments(parents.to_owned(), proc.to_owned(), arguments.len()) }); @@ -2267,21 +2798,21 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { } } - fn pick_arguments(&mut self) -> Status, Expression)]>> { + fn pick_arguments(&mut self) -> Status> { leading!(self.exact(Token::Punct(Punctuation::LParen))); - success(require!(self.separated( - Punctuation::Comma, - Punctuation::RParen, - None, - |this| { - let expr = leading!(this.expression()); - if let Some(()) = this.exact(Token::Punct(Punctuation::Semicolon))? { - success((Some(expr), require!(this.expression()))) - } else { - success((None, expr)) - } - } - )).into()) + success( + require!( + self.separated(Punctuation::Comma, Punctuation::RParen, None, |this| { + let expr = leading!(this.expression()); + if let Some(()) = this.exact(Token::Punct(Punctuation::Semicolon))? { + success((Some(expr), require!(this.expression()))) + } else { + success((None, expr)) + } + }) + ) + .into(), + ) } fn separated Status>( @@ -2319,33 +2850,48 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { fn read_any_tt(&mut self, target: &mut Vec) -> Status<()> { // read a single arbitrary "token tree", either a group or a single token - let start = self.next("anything")?; + if self.peek() == &Token::Eof { + return try_another(); + } + let start = self.take(); let kind = TTKind::from_token(&start); target.push(LocatedToken::new(self.location(), start)); let kind = match kind { Some(k) => k, None => return SUCCESS, }; + let location = self.location; loop { - let token = self.next("anything")?; - if kind.is_end(&token) { - target.push(LocatedToken::new(self.location(), token)); + self.expected(kind.end()); + if kind.is_end(self.peek()) { + target.push(LocatedToken::new(self.location(), self.take())); return SUCCESS; } else { - self.put_back(token); + self.skipping_location = Some(location); require!(self.read_any_tt(target)); + self.skipping_location = None; } } } } -fn reconstruct_path(node: &str, proc_kind: Option, var_type: Option<&VarTypeBuilder>, last: &str) -> Vec { +fn reconstruct_path( + node: &str, + proc_deets: Option, + var_type: Option<&VarTypeBuilder>, + last: &str, +) -> Vec { let mut result = Vec::new(); for entry in node.split('/').skip(1) { result.push(entry.to_owned()); } - if let Some(kind) = proc_kind { - result.push(kind.to_string()); + if let Some(deets) = proc_deets { + result.push(deets.kind.to_string()); + deets + .flags + .to_vec() + .into_iter() + .for_each(|elem| result.push(elem.to_string())); } if let Some(var) = var_type { result.extend(var.flags.to_vec().into_iter().map(ToOwned::to_owned)); diff --git a/crates/dreammaker/src/preprocessor.rs b/crates/dreammaker/src/preprocessor.rs index a681eadc..2d3538b3 100644 --- a/crates/dreammaker/src/preprocessor.rs +++ b/crates/dreammaker/src/preprocessor.rs @@ -1,19 +1,20 @@ //! The preprocessor. -use std::collections::{HashMap, VecDeque}; -use std::{io, fmt}; +use std::borrow::Cow; +use std::collections::VecDeque; use std::fs::File; use std::path::{Path, PathBuf}; -use std::borrow::Cow; +use std::rc::Rc; +use std::{fmt, io}; -use ahash::RandomState; +use foldhash::HashMap; -use interval_tree::{IntervalTree, range}; +use interval_tree::{range, IntervalTree}; -use super::{DMError, Location, HasLocation, FileId, Context, Severity}; -use super::lexer::*; -use super::docs::{DocComment, DocTarget, DocCollection}; use super::annotation::*; use super::ast::Ident; +use super::docs::{CommentKind, DocCollection, DocComment, DocTarget}; +use super::lexer::*; +use super::{Context, DMError, FileId, HasLocation, Location, Severity}; /// The maximum recursion depth of macro expansion. const MAX_RECURSION_DEPTH: usize = 32; @@ -25,13 +26,13 @@ const MAX_RECURSION_DEPTH: usize = 32; pub enum Define { Constant { subst: Vec, - docs: DocCollection, + docs: Rc, }, Function { params: Vec, subst: Vec, variadic: bool, - docs: DocCollection, + docs: Rc, }, } @@ -94,8 +95,16 @@ pub struct DefineHistory { impl DefineHistory { /// Branch a child preprocessor from this preprocessor's historic state at /// the start of the given file. - pub fn branch_at_file<'ctx2>(&self, file: FileId, context: &'ctx2 Context) -> Preprocessor<'ctx2> { - let location = Location { file, line: 0, column: 0 }; + pub fn branch_at_file<'ctx2>( + &self, + file: FileId, + context: &'ctx2 Context, + ) -> Preprocessor<'ctx2> { + let location = Location { + file, + line: 0, + column: 0, + }; let defines = DefineMap::from_history(self, location); Preprocessor { @@ -103,19 +112,19 @@ impl DefineHistory { env_file: self.env_file.clone(), include_stack: Default::default(), include_locations: Default::default(), - history: Default::default(), // TODO: support branching a second time + multiple_locations: Default::default(), + history: Default::default(), // TODO: support branching a second time defines, maps: Default::default(), skins: Default::default(), scripts: Default::default(), - ifdef_stack: Default::default(), // should be fine + ifdef_stack: Default::default(), // should be fine ifdef_history: Default::default(), last_input_loc: location, last_printable_input_loc: location, output: Default::default(), danger_idents: Default::default(), docs_in: Default::default(), - docs_out: Default::default(), in_interp_string: 0, annotations: None, } @@ -128,19 +137,19 @@ impl DefineHistory { env_file: self.env_file.clone(), include_stack: Default::default(), include_locations: Default::default(), - history: Default::default(), // TODO: support branching a second time + multiple_locations: Default::default(), + history: Default::default(), // TODO: support branching a second time defines: DefineMap::from_history(self, self.last_input_loc), maps: Default::default(), skins: Default::default(), scripts: Default::default(), - ifdef_stack: Default::default(), // should be fine + ifdef_stack: Default::default(), // should be fine ifdef_history: Default::default(), last_input_loc: self.last_input_loc, last_printable_input_loc: self.last_input_loc, output: Default::default(), danger_idents: Default::default(), docs_in: Default::default(), - docs_out: Default::default(), in_interp_string: 0, annotations: None, } @@ -168,7 +177,7 @@ impl std::ops::DerefMut for DefineHistory { /// stack is exhausted. #[derive(Debug, Clone, Default)] pub struct DefineMap { - inner: HashMap, RandomState>, + inner: HashMap>, } impl DefineMap { @@ -192,7 +201,7 @@ impl DefineMap { /// Returns true if the map contains a value for the specified key. pub fn contains_key(&self, key: &str) -> bool { - self.inner.get(key).map_or(false, |v| !v.is_empty()) + self.inner.get(key).is_some_and(|v| !v.is_empty()) } /// Returns a reference to the value corresponding to the key. @@ -205,7 +214,10 @@ impl DefineMap { /// Returns `None` if the key was not present, or its most recent location /// if it was. pub fn insert(&mut self, key: String, value: (Location, Define)) -> Option { - let stack = self.inner.entry(key).or_insert_with(|| Vec::with_capacity(1)); + let stack = self + .inner + .entry(key) + .or_insert_with(|| Vec::with_capacity(1)); let result = stack.last().map(|&(loc, _)| loc); stack.push(value); result @@ -220,7 +232,7 @@ impl DefineMap { Some(stack) => { result = stack.pop(); remove = stack.is_empty(); - } + }, } if remove { self.inner.remove(key); @@ -231,7 +243,7 @@ impl DefineMap { /// Cut a DefineMap from the state of a DefineHistory at the given location. fn from_history(history: &InnerDefineHistory, location: Location) -> DefineMap { let mut map = DefineMap::default(); - for (range, &(ref name, ref define)) in history.range(range(location, location)) { + for (range, (name, define)) in history.range(range(location, location)) { map.insert(name.clone(), (range.start, define.clone())); } map @@ -283,7 +295,11 @@ impl<'ctx> Include<'ctx> { }) } - fn from_buffer(context: &'ctx Context, path: PathBuf, buffer: Cow<'ctx, [u8]>) -> Include<'ctx> { + fn from_buffer( + context: &'ctx Context, + path: PathBuf, + buffer: Cow<'ctx, [u8]>, + ) -> Include<'ctx> { let idx = context.register_file(&path); Include::File { //file: idx, @@ -319,18 +335,21 @@ impl<'ctx> Iterator for IncludeStack<'ctx> { fn next(&mut self) -> Option { loop { match self.stack.last_mut() { - Some(&mut Include::File { ref mut lexer, .. }) => match lexer.next() { - //Some(Err(e)) => return Some(Err(e)), - Some(t) => return Some(t), - None => {} // fall through + Some(&mut Include::File { ref mut lexer, .. }) => { + if let Some(t) = lexer.next() { + return Some(t); + } + // else fallthrough to pop() }, Some(&mut Include::Expansion { ref mut tokens, location, .. - }) => match tokens.pop_front() { - Some(token) => return Some(LocatedToken { location, token }), - None => {} // fall through + }) => { + if let Some(token) = tokens.pop_front() { + return Some(LocatedToken { location, token }); + } + // else fallthrough to pop() }, None => return None, } @@ -380,9 +399,12 @@ pub struct Preprocessor<'ctx> { env_file: PathBuf, include_stack: IncludeStack<'ctx>, - include_locations: HashMap, + include_locations: HashMap, + // list of files with #pragma multiple to allow for more then one include + // should this be done as an enum in include_locations instead? + multiple_locations: HashMap, last_input_loc: Location, - output: VecDeque, + output: VecDeque, ifdef_stack: Vec, ifdef_history: IntervalTree, annotations: Option, @@ -394,19 +416,18 @@ pub struct Preprocessor<'ctx> { scripts: Vec, last_printable_input_loc: Location, - danger_idents: HashMap, + danger_idents: HashMap, in_interp_string: u32, docs_in: VecDeque<(Location, DocComment)>, - docs_out: VecDeque<(Location, DocComment)>, } impl<'ctx> HasLocation for Preprocessor<'ctx> { fn location(&self) -> Location { match self.include_stack.stack.last() { - Some(&Include::File { ref lexer, .. }) => lexer.location(), + Some(Include::File { lexer, .. }) => lexer.location(), Some(&Include::Expansion { location, .. }) => location, - None => Location::default() + None => Location::default(), } } } @@ -421,8 +442,11 @@ impl<'ctx> Preprocessor<'ctx> { Ok(Preprocessor { context, env_file, - include_stack: IncludeStack { stack: vec![include] }, + include_stack: IncludeStack { + stack: vec![include], + }, include_locations: Default::default(), + multiple_locations: Default::default(), history: Default::default(), defines: DefineMap::with_builtins(), maps: Default::default(), @@ -435,13 +459,16 @@ impl<'ctx> Preprocessor<'ctx> { output: Default::default(), danger_idents: Default::default(), docs_in: Default::default(), - docs_out: Default::default(), in_interp_string: 0, annotations: None, }) } - pub fn from_buffer>>(context: &'ctx Context, env_file: PathBuf, buffer: S) -> Self { + pub fn from_buffer>>( + context: &'ctx Context, + env_file: PathBuf, + buffer: S, + ) -> Self { let cow_u8 = match buffer.into() { Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()), Cow::Owned(s) => Cow::Owned(s.into_bytes()), @@ -450,8 +477,11 @@ impl<'ctx> Preprocessor<'ctx> { Preprocessor { context, env_file, - include_stack: IncludeStack { stack: vec![include] }, + include_stack: IncludeStack { + stack: vec![include], + }, include_locations: Default::default(), + multiple_locations: Default::default(), history: Default::default(), defines: DefineMap::with_builtins(), maps: Default::default(), @@ -464,7 +494,6 @@ impl<'ctx> Preprocessor<'ctx> { output: Default::default(), danger_idents: Default::default(), docs_in: Default::default(), - docs_out: Default::default(), in_interp_string: 0, annotations: None, } @@ -483,7 +512,8 @@ impl<'ctx> Preprocessor<'ctx> { line: !0, column: i, }; - self.history.insert(range(start, end), (name.clone(), define)); + self.history + .insert(range(start, end), (name.clone(), define)); } } DefineHistory { @@ -509,7 +539,11 @@ impl<'ctx> Preprocessor<'ctx> { */ /// Push a DM file to the top of this preprocessor's stack. - pub fn push_file(&mut self, path: PathBuf, read: R) -> Result { + pub fn push_file( + &mut self, + path: PathBuf, + read: R, + ) -> Result { let idx = self.context.register_file(&path); self.include_stack.stack.push(Include::File { lexer: Lexer::from_read(self.context, idx, read)?, @@ -532,15 +566,25 @@ impl<'ctx> Preprocessor<'ctx> { // ------------------------------------------------------------------------ // Macro definition handling - fn annotate_macro(&mut self, ident: &str, def_loc: Location) { + fn annotate_macro( + &mut self, + ident: &str, + definition_location: Location, + docs: Option>, + ) { if self.include_stack.in_expansion() { return; } if let Some(annotations) = self.annotations.as_mut() { annotations.insert( - self.last_input_loc .. self.last_input_loc.add_columns(ident.len() as u16), - Annotation::MacroUse(ident.to_owned(), def_loc)); + self.last_input_loc..self.last_input_loc.add_columns(ident.len() as u16), + Annotation::MacroUse { + name: ident.to_owned(), + definition_location, + docs, + }, + ); } } @@ -561,7 +605,8 @@ impl<'ctx> Preprocessor<'ctx> { } fn move_to_history(&mut self, name: String, previous: (Location, Define)) { - self.history.insert(range(previous.0, self.last_input_loc), (name, previous.1)); + self.history + .insert(range(previous.0, self.last_input_loc), (name, previous.1)); } // ------------------------------------------------------------------------ @@ -572,9 +617,9 @@ impl<'ctx> Preprocessor<'ctx> { } fn pop_ifdef(&mut self) -> Option { - self.ifdef_stack.pop().map(|ifdef| { - self.ifdef_history.insert(range(ifdef.location, self.last_input_loc), ifdef.active); - ifdef + self.ifdef_stack.pop().inspect(|ifdef| { + self.ifdef_history + .insert(range(ifdef.location, self.last_input_loc), ifdef.active); }) } @@ -600,12 +645,16 @@ impl<'ctx> Preprocessor<'ctx> { return Ok(false); } - let expr = crate::parser::parse_expression( - self.context, - start, - self.output.drain(..).map(|token| LocatedToken::new(start, token)) - )?; - Ok(crate::constants::preprocessor_evaluate(start, expr, &self.defines)?.to_bool()) + let expr = crate::parser::parse_expression(self.context, start, self.output.drain(..))?; + Ok( + crate::constants::preprocessor_evaluate( + start, + expr, + &self.defines, + Some(self.context), + )? + .to_bool(), + ) } fn evaluate(&mut self) -> bool { @@ -615,7 +664,7 @@ impl<'ctx> Preprocessor<'ctx> { Err(err) => { self.context.register_error(err); false - } + }, } } @@ -624,7 +673,13 @@ impl<'ctx> Preprocessor<'ctx> { /// Something other than a `#define` was encountered, docs are not for us. fn flush_docs(&mut self) { - self.docs_out.extend(self.docs_in.drain(..)); + if !self.docs_in.is_empty() { + self.output.extend( + self.docs_in + .drain(..) + .map(|(l, d)| LocatedToken::new(l, Token::DocComment(d))), + ); + } } // ------------------------------------------------------------------------ @@ -632,38 +687,50 @@ impl<'ctx> Preprocessor<'ctx> { fn prepare_include_file(&mut self, path: PathBuf) -> Result, DMError> { // Attempt to open the file. - let read = io::BufReader::new(File::open(&path).map_err(|e| - DMError::new(self.last_input_loc, format!("failed to open file: #include {:?}", path)) - .with_cause(e))?); + let read = io::BufReader::new(File::open(&path).map_err(|e| { + DMError::new( + self.last_input_loc, + format!("failed to open file: #include {path:?}"), + ) + .with_cause(e) + })?); // Get the path relative to the environment root, for easy lookup later. - let register = path.strip_prefix(self.env_file.parent().unwrap()).unwrap_or(&path); + let register = path + .strip_prefix(self.env_file.parent().unwrap()) + .unwrap_or(&path); // Make sure the file hasn't already been included. // All DM source is effectively `#pragma once`. - let file_id = self.context.register_file(®ister); + let file_id = self.context.register_file(register); if let Some(&loc) = self.include_locations.get(&file_id) { - Err(DMError::new(self.last_input_loc, format!("duplicate #include {:?}", path)) - .set_severity(Severity::Warning) - .with_note(loc, "previously included here") - .with_errortype("duplicate_include")) + if !self.multiple_locations.contains_key(&file_id) { + Err( + DMError::new(self.last_input_loc, format!("duplicate #include {path:?}")) + .set_severity(Severity::Warning) + .with_note(loc, "previously included here") + .with_errortype("duplicate_include"), + ) + } else { + Ok(Include::File { + path, + //file: file_id, + lexer: Lexer::from_read(self.context, file_id, read)?, + }) + } } else { self.include_locations.insert(file_id, self.last_input_loc); Ok(Include::File { path, //file: file_id, - lexer: Lexer::from_read(&self.context, file_id, read)?, + lexer: Lexer::from_read(self.context, file_id, read)?, }) } } - fn check_danger_ident(&mut self, name: &str, kind: &str) { - if let Some(loc) = self.danger_idents.get(name) { - self.context.register_error(DMError::new(*loc, format!( - "macro {:?} used immediately before being {}:\n\ - https://secure.byond.com/forum/?post=2072419", name, kind - )).set_severity(Severity::Warning)); - } + fn push_output(&mut self, token: Token) { + self.output + .push_back(LocatedToken::new(self.last_input_loc, token)); } fn inner_next(&mut self) -> Option { @@ -679,7 +746,7 @@ impl<'ctx> Preprocessor<'ctx> { Some(x) => { _last_expected_loc = x.location; x.token - } + }, None => return Err(self.error("unexpected EOF")), } }; @@ -693,9 +760,10 @@ impl<'ctx> Preprocessor<'ctx> { } } + #[rustfmt::skip] const ALL_DIRECTIVES: &[&str] = &[ "if", "ifdef", "ifndef", "elif", "else", "endif", - "include", "define", "undef", "warn", "error", + "include", "define", "undef", "warn", "error", "pragma", ]; let disabled = !inside_condition && self.is_disabled(); match read { @@ -705,50 +773,60 @@ impl<'ctx> Preprocessor<'ctx> { match &ident[..] { // ifdefs "endif" => { - self.pop_ifdef().ok_or_else(|| - DMError::new(self.last_input_loc, "unmatched #endif"))?; - } + self.pop_ifdef() + .ok_or_else(|| DMError::new(self.last_input_loc, "unmatched #endif"))?; + }, "else" => { - let last = self.pop_ifdef().ok_or_else(|| - DMError::new(self.last_input_loc, "unmatched #else"))?; + let last = self + .pop_ifdef() + .ok_or_else(|| DMError::new(self.last_input_loc, "unmatched #else"))?; self.ifdef_stack.push(last.else_(self.last_input_loc)); - } + }, "ifdef" => { expect_token!((define_name) = Token::Ident(define_name, _)); expect_token!(() = Token::Punct(Punctuation::Newline)); let enabled = self.is_defined(&define_name); - self.ifdef_stack.push(Ifdef::new(self.last_input_loc, enabled)); - } + self.ifdef_stack + .push(Ifdef::new(self.last_input_loc, enabled)); + }, "ifndef" => { expect_token!((define_name) = Token::Ident(define_name, _)); expect_token!(() = Token::Punct(Punctuation::Newline)); let enabled = !self.is_defined(&define_name); - self.ifdef_stack.push(Ifdef::new(self.last_input_loc, enabled)); - } + self.ifdef_stack + .push(Ifdef::new(self.last_input_loc, enabled)); + }, "if" => { let enabled = self.evaluate(); - self.ifdef_stack.push(Ifdef::new(self.last_input_loc, enabled)); - } + self.ifdef_stack + .push(Ifdef::new(self.last_input_loc, enabled)); + }, "elif" => { - let last = self.pop_ifdef().ok_or_else(|| - DMError::new(self.last_input_loc, "unmatched #elif"))?; + let last = self + .pop_ifdef() + .ok_or_else(|| DMError::new(self.last_input_loc, "unmatched #elif"))?; let enabled = self.evaluate(); - self.ifdef_stack.push(last.else_if(self.last_input_loc, enabled)); - } + self.ifdef_stack + .push(last.else_if(self.last_input_loc, enabled)); + }, // -------------------------------------------------------- // anything other than ifdefs may be ifdef'd out // -------------------------------------------------------- // include searches relevant paths for files - "include" if disabled => {} + "include" if disabled => {}, "include" => { expect_token!((path_str) = Token::String(path_str)); let include_loc = _last_expected_loc; expect_token!(() = Token::Punct(Punctuation::Newline)); - let path = PathBuf::from(path_str.replace("\\", "/")); + let path = PathBuf::from(path_str.replace('\\', "/")); - for candidate in vec![ + for candidate in [ // 1. relative to file in which `#include` appears. - self.include_stack.top_file_path().parent().unwrap().join(&path), + self.include_stack + .top_file_path() + .parent() + .unwrap() + .join(&path), // 2. relative to root `.dme` file. self.env_file.parent().unwrap().join(&path), ] { @@ -757,6 +835,8 @@ impl<'ctx> Preprocessor<'ctx> { } // Double-match is used to let go of the borrow of // `candidate` so it can be used in the second half. + // This is how BYOND refers to it's file formats, this is how we should refer to them. + #[allow(clippy::upper_case_acronyms)] enum FileType { DMM, DMF, @@ -772,20 +852,24 @@ impl<'ctx> Preprocessor<'ctx> { Some(ext) => { self.context.register_error(DMError::new( self.last_input_loc, - format!("unknown extension {:?}", ext), + format!("unknown extension {ext:?}"), )); return Ok(()); - } + }, None => { - self.context.register_error(DMError::new(self.last_input_loc, "filename has no extension")); + self.context.register_error(DMError::new( + self.last_input_loc, + "filename has no extension", + )); return Ok(()); - } + }, }; if let Some(annotations) = self.annotations.as_mut() { annotations.insert( - include_loc .. include_loc.add_columns(2 + path_str.len() as u16), - Annotation::Include(candidate.clone())); + include_loc..include_loc.add_columns(2 + path_str.len() as u16), + Annotation::Include(candidate.clone()), + ); } match file_type { @@ -797,7 +881,7 @@ impl<'ctx> Preprocessor<'ctx> { // A phantom newline keeps the include // directive being indented from making // the first line of the file indented. - self.output.push_back(Token::Punct(Punctuation::Newline)); + self.push_output(Token::Punct(Punctuation::Newline)); self.include_stack.stack.push(include); }, Err(e) => self.context.register_error(e), @@ -807,50 +891,40 @@ impl<'ctx> Preprocessor<'ctx> { return Ok(()); } - self.context.register_error(DMError::new(self.last_input_loc, format!("failed to find #include {:?}", path))); + self.context.register_error(DMError::new( + self.last_input_loc, + format!("failed to find #include {path:?}"), + )); return Ok(()); - } + }, // both constant and function defines "define" if disabled => { // Skip to the end of the line, or else we'll catch // stringify operators `#X` as unknown directives. loop { - match next!() { - Token::Punct(Punctuation::Newline) => break, - _ => {} - } - } - } - "define" => { - // accumulate just-seen Following doc comments - let mut our_docs = Vec::new(); - while let Some((loc, doc)) = self.docs_in.pop_back() { - if doc.target == DocTarget::FollowingItem { - our_docs.push(doc); - } else { - self.docs_in.push_back((loc, doc)); + if let Token::Punct(Punctuation::Newline) = next!() { break; } } + }, + "define" => { + // accumulate just-seen Following doc comments let mut docs = DocCollection::default(); - for each in our_docs.into_iter().rev() { - docs.push(each); - } - // flush all docs which do not apply to this define - self.flush_docs(); + docs.extend(self.docs_in.drain(..).map(|x| x.1)); expect_token!((define_name, ws) = Token::Ident(define_name, ws)); let define_name_loc = _last_expected_loc; if let Some(annotations) = self.annotations.as_mut() { annotations.insert( - define_name_loc .. define_name_loc.add_columns(define_name.len() as u16), - Annotation::MacroDefinition(define_name.to_owned())); + define_name_loc + ..define_name_loc.add_columns(define_name.len() as u16), + Annotation::MacroDefinition(define_name.to_owned()), + ); } - self.check_danger_ident(&define_name, "defined"); let mut params = Vec::new(); let mut subst = Vec::new(); let mut variadic = false; - 'outer: loop { + 'outer: { match next!() { Token::Punct(Punctuation::LParen) if !ws => { loop { @@ -860,13 +934,17 @@ impl<'ctx> Preprocessor<'ctx> { match next!() { Token::Ident(name, _) => params.push(name), Token::Punct(Punctuation::Ellipsis) => { - params.push("__VA_ARGS__".to_owned()); // default + params.push("__VA_ARGS__".to_owned()); // default variadic = true; - } - _ => return Err(self.error("malformed macro parameters, expected name")), + }, + _ => { + return Err(self.error( + "malformed macro parameters, expected name", + )) + }, } match next!() { - Token::Punct(Punctuation::Comma) => {} + Token::Punct(Punctuation::Comma) => {}, Token::Punct(Punctuation::RParen) => break, Token::Punct(Punctuation::Ellipsis) => { variadic = true; @@ -874,72 +952,136 @@ impl<'ctx> Preprocessor<'ctx> { Token::Punct(Punctuation::RParen) => break, _ => return Err(self.error("only the last parameter of a macro may be variadic")) } - } - _ => return Err(self.error("malformed macro parameters, expected comma")), + }, + _ => { + return Err(self.error( + "malformed macro parameters, expected comma", + )) + }, } } - } + }, Token::Punct(Punctuation::Newline) => break 'outer, - Token::DocComment(doc) => docs.push(doc), + Token::DocComment(doc) => { + if doc.target != DocTarget::EnclosingItem { + let message = match doc.kind { + CommentKind::Block => { + "after a #define, should be `/*!` instead of `/**`" + }, + CommentKind::Line => { + "after a #define, should be `//!` instead of `///`" + }, + }; + self.error(message) + .set_severity(Severity::Hint) + .register(self.context); + } + docs.push(doc); + }, other => { subst.push(other); - } + }, } loop { match next!() { Token::Punct(Punctuation::Newline) => break 'outer, - Token::DocComment(doc) => docs.push(doc), + Token::DocComment(doc) => { + if doc.target != DocTarget::EnclosingItem { + let message = match doc.kind { + CommentKind::Block => "after a #define, should be `/*!` instead of `/**`", + CommentKind::Line => "after a #define, should be `//!` instead of `///`", + }; + self.error(message) + .set_severity(Severity::Hint) + .register(self.context); + } + docs.push(doc); + }, other => subst.push(other), } } } let define = if params.is_empty() { - Define::Constant { subst, docs } + Define::Constant { + subst, + docs: Rc::new(docs), + } } else { - Define::Function { params, subst, variadic, docs } + Define::Function { + params, + subst, + variadic, + docs: Rc::new(docs), + } }; // DEBUG can only be defined in the root .dme file if define_name != "DEBUG" || self.in_environment() { - if let Some(previous_loc) = self.defines.insert(define_name.clone(), (define_name_loc, define)) { + if let Some(previous_loc) = self + .defines + .insert(define_name.clone(), (define_name_loc, define)) + { // DM doesn't issue a warning for this, but it's usually a mistake, so let's. // FILE_DIR is handled specially and sometimes makes sense to define multiple times. if define_name != "FILE_DIR" { - DMError::new(define_name_loc, format!("macro redefined: {}", define_name)) - .set_severity(Severity::Warning) - .with_note(previous_loc, format!("previous definition of {}", define_name)) - .with_errortype("macro_redefined") - .register(self.context); + DMError::new( + define_name_loc, + format!("macro redefined: {define_name}"), + ) + .set_severity(Severity::Warning) + .with_note( + previous_loc, + format!("previous definition of {define_name}"), + ) + .with_errortype("macro_redefined") + .register(self.context); } } } - } - "undef" if disabled => {} + }, + "undef" if disabled => {}, "undef" => { expect_token!((define_name) = Token::Ident(define_name, _)); let define_name_loc = _last_expected_loc; - self.check_danger_ident(&define_name, "undefined"); expect_token!(() = Token::Punct(Punctuation::Newline)); if let Some(previous) = self.defines.remove(&define_name) { self.move_to_history(define_name, previous); } else { - DMError::new(define_name_loc, format!("macro undefined while not defined: {}", define_name)) - .with_errortype("macro_undefined_no_definition") - .set_severity(Severity::Warning) - .register(self.context); - } - } - "warn" if disabled => {} - "warn" => { - expect_token!((text) = Token::String(text)); - DMError::new(self.last_input_loc, format!("#{} {}", ident, text)) + DMError::new( + define_name_loc, + format!("macro undefined while not defined: {define_name}"), + ) + .with_errortype("macro_undefined_no_definition") .set_severity(Severity::Warning) .register(self.context); - } - "error" if disabled => {} + } + }, + "warn" if disabled => {}, + "warn" => { + expect_token!((text) = Token::String(text)); + DMError::new( + self.last_input_loc, + format!("#{} {}", ident, text.trim_end_matches(['\r', '\n'])), + ) + .set_severity(Severity::Warning) + .register(self.context); + }, + "error" if disabled => {}, "error" => { expect_token!((text) = Token::String(text)); - self.context.register_error(DMError::new(self.last_input_loc, format!("#{} {}", ident, text))); - } + self.context.register_error(DMError::new( + self.last_input_loc, + format!("#{} {}", ident, text.trim_end_matches(['\r', '\n'])), + )); + }, + "pragma" if disabled => {}, + "pragma" => { + expect_token!((text) = Token::Ident(text, _)); + let pragma_use_loc = _last_expected_loc; + if text.as_str() == "multiple" { + self.multiple_locations + .insert(pragma_use_loc.file, pragma_use_loc); + } + }, // none of this other stuff should even exist other => { let mut meant = ""; @@ -948,14 +1090,25 @@ impl<'ctx> Preprocessor<'ctx> { meant = each; } } - return Err(DMError::new(self.last_input_loc, format!("unknown directive: #{}{}{}", ident, - if !meant.is_empty() { ", did you mean #" } else { "" }, meant))); - } + return Err(DMError::new( + self.last_input_loc, + format!( + "unknown directive: #{}{}{}", + ident, + if !meant.is_empty() { + ", did you mean #" + } else { + "" + }, + meant + ), + )); + }, } // yield a newline - self.output.push_back(Token::Punct(Punctuation::Newline)); + self.push_output(Token::Punct(Punctuation::Newline)); return Ok(()); - } + }, // anything other than directives may be ifdef'd out _ if disabled => return Ok(()), // identifiers may be macros @@ -964,32 +1117,41 @@ impl<'ctx> Preprocessor<'ctx> { // lint for BYOND bug if self.in_interp_string > 0 { - self.danger_idents.insert(ident.clone(), self.last_input_loc); + self.danger_idents + .insert(ident.clone(), self.last_input_loc); } // substitute special macros if ident == "__FILE__" { - self.annotate_macro(ident, Location::builtins()); + self.annotate_macro(ident, Location::builtins(), None); for include in self.include_stack.stack.iter().rev() { if let Include::File { ref path, .. } = *include { - self.output.push_back(Token::String(path.display().to_string())); + self.push_output(Token::String(path.display().to_string())); return Ok(()); } } - self.output.push_back(Token::String(String::new())); + self.push_output(Token::String(String::new())); return Ok(()); } else if ident == "__LINE__" { - self.annotate_macro(ident, Location::builtins()); - self.output.push_back(Token::Int(self.last_input_loc.line as i32)); + self.annotate_macro(ident, Location::builtins(), None); + self.push_output(Token::Int(self.last_input_loc.line as i32)); return Ok(()); } // special case for inside a defined() call - if let Some(Token::Punct(Punctuation::LParen)) = self.output.back() { + if let Some(LocatedToken { + token: Token::Punct(Punctuation::LParen), + .. + }) = self.output.back() + { if let Some(idx) = self.output.len().checked_sub(2) { - if let Some(Token::Ident(identname, _)) = self.output.get(idx) { + if let Some(LocatedToken { + token: Token::Ident(identname, _), + .. + }) = self.output.get(idx) + { if identname.as_str() == "defined" { - self.output.push_back(Token::Ident(ident.to_owned(), whitespace)); + self.push_output(Token::Ident(ident.to_owned(), whitespace)); return Ok(()); } } @@ -997,40 +1159,47 @@ impl<'ctx> Preprocessor<'ctx> { } // if it's a define, perform the substitution - let mut expansion = self.defines.get(ident).cloned(); // TODO: don't clone? + let mut expansion = self.defines.get(ident).cloned(); // TODO: don't clone? if expansion.is_some() && self.include_stack.stack.len() > MAX_RECURSION_DEPTH { - self.error(format!("expanding {:?} would exceed max recursion depth of {} levels", - ident, MAX_RECURSION_DEPTH)).register(self.context); + self.error(format!("expanding {ident:?} would exceed max recursion depth of {MAX_RECURSION_DEPTH} levels")).register(self.context); expansion = None; } match expansion { - Some((location, Define::Constant { subst, docs: _ })) => { - self.annotate_macro(ident, location); + Some((location, Define::Constant { subst, docs })) => { + self.annotate_macro(ident, location, Some(docs)); self.include_stack.stack.push(Include::Expansion { //name: ident.to_owned(), tokens: subst.into_iter().collect(), location: self.last_input_loc, }); return Ok(()); - } - Some((location, Define::Function { ref params, ref subst, variadic, docs: _ })) => { + }, + Some(( + location, + Define::Function { + ref params, + ref subst, + variadic, + docs, + }, + )) => { // if it's not followed by an LParen, it isn't really a function call match next!() { - Token::Punct(Punctuation::LParen) => {} + Token::Punct(Punctuation::LParen) => {}, other => { - self.output.push_back(Token::Ident(ident.to_owned(), false)); + self.push_output(Token::Ident(ident.to_owned(), false)); match other { Token::InterpStringBegin(_) => self.in_interp_string += 1, Token::InterpStringEnd(_) => self.in_interp_string -= 1, - _ => {} + _ => {}, } - self.output.push_back(other); + self.push_output(other); return Ok(()); - } + }, } - self.annotate_macro(ident, location); + self.annotate_macro(ident, location, Some(docs)); // read arguments let mut args = Vec::new(); @@ -1042,7 +1211,7 @@ impl<'ctx> Preprocessor<'ctx> { Token::Punct(Punctuation::LParen) => { parens += 1; this_arg.push(token); - } + }, Token::Punct(Punctuation::RParen) => { if parens == 0 { args.push(this_arg); @@ -1050,11 +1219,11 @@ impl<'ctx> Preprocessor<'ctx> { } parens -= 1; this_arg.push(token); - } + }, Token::Punct(Punctuation::Comma) if parens == 0 => { args.push(this_arg); this_arg = Vec::new(); - } + }, _ => this_arg.push(token), } } @@ -1062,7 +1231,9 @@ impl<'ctx> Preprocessor<'ctx> { // check for correct number of arguments if variadic { if args.len() > params.len() { - let new_arg = args.split_off(params.len() - 1).join(&Token::Punct(Punctuation::Comma)); + let new_arg = args + .split_off(params.len() - 1) + .join(&Token::Punct(Punctuation::Comma)); args.push(new_arg); } else if args.len() + 1 == params.len() { args.push(Vec::new()); @@ -1078,59 +1249,69 @@ impl<'ctx> Preprocessor<'ctx> { while let Some(token) = input.next() { match token { // just an ident = expand it - Token::Ident(ident, ws) => match params.iter().position(|x| *x == ident) { - Some(i) => expansion.extend(args[i].iter().cloned()), - None => expansion.push_back(Token::Ident(ident, ws)), + Token::Ident(ident, ws) => { + match params.iter().position(|x| *x == ident) { + Some(i) => expansion.extend(args[i].iter().cloned()), + None => expansion.push_back(Token::Ident(ident, ws)), + } }, // token paste = concat two idents together, if at all possible Token::Punct(Punctuation::TokenPaste) => { match (expansion.pop_back(), input.next()) { - (Some(Token::Ident(first, ws1)), Some(Token::Ident(param_name, ws))) => { - match params.iter().position(|x| *x == param_name) { - Some(i) => { - let mut arg = args[i].iter().cloned(); - match arg.next() { - Some(Token::Ident(param_ident, ws)) => { - expansion.push_back(Token::Ident( - format!("{}{}", first, param_ident), - ws, - )); - } - Some(Token::Int(param_int)) => { - expansion.push_back(Token::Ident( - format!("{}{}", first, param_int), - ws, - )) - } - Some(other) => { - expansion.push_back(Token::Ident(first, ws1)); - expansion.push_back(other); - } - None => {} - } - expansion.extend(arg); + ( + Some(Token::Ident(first, ws1)), + Some(Token::Ident(param_name, ws)), + ) => match params.iter().position(|x| *x == param_name) { + Some(i) => { + let mut arg = args[i].iter().cloned(); + match arg.next() { + Some(Token::Ident(param_ident, ws)) => { + expansion.push_back(Token::Ident( + format!("{first}{param_ident}"), + ws, + )); + }, + Some(Token::Int(param_int)) => expansion + .push_back(Token::Ident( + format!("{first}{param_int}"), + ws, + )), + Some(other) => { + expansion + .push_back(Token::Ident(first, ws1)); + expansion.push_back(other); + }, + None => {}, } - None => expansion.push_back(Token::Ident(format!("{}{}", first, param_name), ws)), - } - } + expansion.extend(arg); + }, + None => expansion.push_back(Token::Ident( + format!("{first}{param_name}"), + ws, + )), + }, (non_ident_first, Some(Token::Ident(second, ws))) => { expansion.extend(non_ident_first); match params.iter().position(|x| *x == second) { - Some(i) => expansion.extend(args[i].iter().cloned()), - None => expansion.push_back(Token::Ident(second, ws)), + Some(i) => { + expansion.extend(args[i].iter().cloned()) + }, + None => { + expansion.push_back(Token::Ident(second, ws)) + }, } - } + }, (non_ident_first, non_ident_second) => { expansion.extend(non_ident_first); expansion.extend(non_ident_second); - } + }, } // read the next ident and concat it into the previous ident }, // hash = must be followed by a param name, stringify the whole argument - Token::Punct(Punctuation::Hash) => { - match input.next() { - Some(Token::Ident(argname, _)) => match params.iter().position(|x| *x == argname) { + Token::Punct(Punctuation::Hash) => match input.next() { + Some(Token::Ident(argname, _)) => { + match params.iter().position(|x| *x == argname) { Some(i) => { let mut string = String::new(); for each in &args[i] { @@ -1138,19 +1319,37 @@ impl<'ctx> Preprocessor<'ctx> { if !string.is_empty() { string.push(' '); } - let _e = write!(string, "{}", each); - #[cfg(debug_assertions)] { + let _e = write!(string, "{each}"); + #[cfg(debug_assertions)] + { _e.unwrap(); } } expansion.push_back(Token::String(string)); - } - None => return Err(DMError::new(self.last_input_loc, format!("can't stringify non-argument ident {:?}", argname))), + }, + None => { + return Err(DMError::new( + self.last_input_loc, + format!( + "can't stringify non-argument ident {argname:?}" + ), + )) + }, } - Some(tok) => return Err(DMError::new(self.last_input_loc, format!("can't stringify non-ident '{}'", tok))), - None => return Err(DMError::new(self.last_input_loc, "can't stringify EOF")), - } - } + }, + Some(tok) => { + return Err(DMError::new( + self.last_input_loc, + format!("can't stringify non-ident '{tok}'"), + )) + }, + None => { + return Err(DMError::new( + self.last_input_loc, + "can't stringify EOF", + )) + }, + }, _ => expansion.push_back(token), } } @@ -1160,24 +1359,24 @@ impl<'ctx> Preprocessor<'ctx> { location: self.last_input_loc, }); return Ok(()); - } - None => {} + }, + None => {}, } - } + }, Token::InterpStringBegin(_) => self.in_interp_string += 1, Token::InterpStringEnd(_) => self.in_interp_string -= 1, // documentation is accumulated, and flushed if no #define follows - Token::DocComment(doc) => { + Token::DocComment(doc) if doc.target == DocTarget::FollowingItem => { self.docs_in.push_back((self.last_input_loc, doc)); return Ok(()); }, // everything else is itself - _ => {} + _ => {}, } if !read.is_whitespace() { self.flush_docs(); } - self.output.push_back(read); + self.push_output(read); Ok(()) } } @@ -1187,25 +1386,15 @@ impl<'ctx> Iterator for Preprocessor<'ctx> { fn next(&mut self) -> Option { loop { - if let Some((location, doc)) = self.docs_out.pop_front() { - return Some(LocatedToken { - location, - token: Token::DocComment(doc), - }); - } - if let Some(token) = self.output.pop_front() { - return Some(LocatedToken { - location: self.last_input_loc, - token, - }); + return Some(token); } if let Some(tok) = self.inner_next() { - // linting for https://secure.byond.com/forum/?post=2072419 + // linting for https://www.byond.com/forum/?post=2072419 if !tok.token.is_whitespace() && tok.token != Token::Punct(Punctuation::Hash) { - if tok.location.file != self.last_printable_input_loc.file || - tok.location.line > self.last_printable_input_loc.line + if tok.location.file != self.last_printable_input_loc.file + || tok.location.line > self.last_printable_input_loc.line { self.danger_idents.clear(); } @@ -1219,7 +1408,8 @@ impl<'ctx> Iterator for Preprocessor<'ctx> { } } else { while let Some(ifdef) = self.pop_ifdef() { - self.context.register_error(DMError::new(ifdef.location, "unterminated #if/#ifdef")); + self.context + .register_error(DMError::new(ifdef.location, "unterminated #if/#ifdef")); } return None; } diff --git a/crates/dreammaker/tests/annotation_tests.rs b/crates/dreammaker/tests/annotation_tests.rs index f26a1ddf..e98c95bc 100644 --- a/crates/dreammaker/tests/annotation_tests.rs +++ b/crates/dreammaker/tests/annotation_tests.rs @@ -1,14 +1,14 @@ extern crate dreammaker as dm; -use dm::Location; -use dm::lexer::*; use dm::annotation::*; -use dm::parser::Parser; use dm::indents::IndentProcessor; +use dm::lexer::*; +use dm::parser::Parser; +use dm::Location; #[test] fn annotation_basic() { - let code = r##" + let code = r#" /var/foo = bar /datum/globals var/number = 7 + 5 @@ -19,7 +19,8 @@ fn annotation_basic() { proc/Init() world.log << new/obj() -"##.trim(); +"# + .trim(); let context = Default::default(); let lexer = Lexer::new(&context, Default::default(), code.as_bytes()); @@ -33,7 +34,7 @@ fn annotation_basic() { line: 9, column: 14, }) { - println!("{:?}", each); + println!("{each:?}"); for each in annotations.get_range_raw(each.0) { println!(" {:?}", each.1); } diff --git a/crates/dreammaker/tests/ast_tests.rs b/crates/dreammaker/tests/ast_tests.rs index 47d03975..b9ac8111 100644 --- a/crates/dreammaker/tests/ast_tests.rs +++ b/crates/dreammaker/tests/ast_tests.rs @@ -1,8 +1,12 @@ extern crate dreammaker as dm; -use dm::*; -use dm::preprocessor::Preprocessor; +use core::panic; + +use dm::ast::*; +use dm::constants::*; use dm::objtree::ObjectTree; +use dm::preprocessor::Preprocessor; +use dm::*; fn with_code(code: &'static str, f: F) { let context = Context::default(); @@ -18,17 +22,356 @@ fn with_code(code: &'static str, f: F) { #[test] fn check_semicolon_in_proc_parameters() { - with_code(" + with_code( + " #define DEF1 0x01; #define DEF2 \"asdf\" as text; /proc/darn(foo = DEF1, bar = DEF2, anotherarg = 1) -", |context, _| { - let errors = context.errors(); - assert_eq!(errors.len(), 2); +", + |context, _| { + let errors = context.errors(); + assert_eq!(errors.len(), 2); - for error in errors.into_iter() { - assert_eq!(error.errortype().expect("No errortype set!"), "semicolon_in_proc_parameter"); - } - }); + for error in errors.iter() { + assert_eq!( + error.errortype().expect("No errortype set!"), + "semicolon_in_proc_parameter" + ); + } + }, + ); +} + +#[test] +fn process_scope() { + with_code( + " +/datum/test + var/hell = type::base + var/base = 10 + var/heck = type::base + var/static/stat = 1 + var/proc_holder = type::reference() + +/datum/test/proc/reference() + +/datum/test/sub + base = parent_type::base + 10 + heck = /datum/test::heck + 2 + +var/global/bill = 1 + +/proc/work() + var/x = /datum/test::base + var/datum/test/larry = /datum/test + x = larry::stat + x = /datum/test::reference() + /datum/test::stat = 2 + ::extra() + x = ::bill + +/proc/extra() +", + |context, tree| { + let errors = context.errors(); + + // Check for errors + let mut sum_errors: Vec = vec![]; + for error in errors.iter() { + sum_errors.push(format!("{error}")); + } + if !sum_errors.is_empty() { + panic!("\n{}", sum_errors.join("\n").as_str()); + } + // test type::var in typedef + let parent_type = tree.find("/datum/test").unwrap(); + let type_read = parent_type.get_value("heck").unwrap(); + let Some(constant) = type_read.constant.as_ref() else { + panic!("Failed to constant evaluate :: operator") + }; + if let Constant::Float(value) = constant { + assert_eq!(*value, 10f32) + } else { + panic!( + "{} was expected to be a float, but it wasn't!", + type_read.constant.as_ref().unwrap() + ) + } + // test type::proc() in typedef + let parent_type = tree.find("/datum/test").unwrap(); + let type_read = parent_type.get_value("proc_holder").unwrap(); + let Some(constant) = type_read.constant.as_ref() else { + panic!("Failed to constant evaluate :: proc operator") + }; + if let Constant::Prefab(value) = constant { + let pop_list = FormatTreePath(&value.path).to_string(); + assert_eq!(pop_list, "/datum/test/proc/reference") + } else { + panic!( + "{} was expected to be a path, but it wasn't!", + type_read.constant.as_ref().unwrap() + ) + } + // parent_type::var in a subtype + let child_type = tree.find("/datum/test/sub").unwrap(); + let type_read = child_type.get_value("base").unwrap(); + let Some(constant) = type_read.constant.as_ref() else { + panic!("Failed to constant evaluate :: operator") + }; + if let Constant::Float(value) = constant { + assert_eq!(*value, 10f32 + 10f32) + } else { + panic!( + "{} was expected to be a float, but it wasn't!", + type_read.constant.as_ref().unwrap() + ) + } + // /datum/explicit::var in a type + let child_type = tree.find("/datum/test/sub").unwrap(); + let type_read = child_type.get_value("heck").unwrap(); + let Some(constant) = type_read.constant.as_ref() else { + panic!("Failed to constant evaluate :: operator") + }; + if let Constant::Float(value) = constant { + assert_eq!(*value, 10f32 + 2f32) + } else { + panic!( + "{} was expected to be a float, but it wasn't!", + type_read.constant.as_ref().unwrap() + ) + } + let global_procs = tree.root(); + let work_proc = global_procs.get_proc("work").unwrap(); + let work_code = work_proc + .code + .as_ref() + .unwrap() + .iter() + .map(|statement| &statement.elem) + .collect::>(); + // /datum/explicit::var + let Statement::Var(x_init) = work_code.first().unwrap() else { + panic!("First statement was not an expression") + }; + let Expression::Base { term: _, follow } = x_init.value.as_ref().unwrap() else { + panic!("/datum/test::base was NOT evaluated as a base expression") + }; + match &follow.first().unwrap().elem { + Follow::StaticField(field) => { + if field != "base" { + panic!("/datum/test::base did not eval base as the var to read") + } + }, + _ => panic!("/datum/test::base failed to eval :: as a static field"), + } + // implicit_type::variable + let Statement::Expr(larrys_read) = work_code.get(2).unwrap() else { + panic!("Third statement was not an expression") + }; + + let Expression::AssignOp { op: _, lhs: _, rhs } = larrys_read else { + panic!("x = larry::stat was NOT evaluated as an assignment expression") + }; + let Expression::Base { + term: _, + ref follow, + } = **rhs + else { + panic!("larry::stat was NOT evaluated as a base expression") + }; + match &follow.first().unwrap().elem { + Follow::StaticField(field) => { + if field != "stat" { + panic!("larry::stat did not eval stat as the var to read") + } + }, + _ => panic!("larry::stat failed to eval :: as a static field"), + } + + // /datum/explicit::proc() + let Statement::Expr(proc_ref) = work_code.get(3).unwrap() else { + panic!("Fourth statement was not an expression") + }; + + let Expression::AssignOp { op: _, lhs: _, rhs } = proc_ref else { + panic!("x = /datum/test::reference() was NOT evaluated as an assignment expression") + }; + let Expression::Base { + term: _, + ref follow, + } = **rhs + else { + panic!("/datum/test::reference() was NOT evaluated as a base expression") + }; + match &follow.first().unwrap().elem { + Follow::ProcReference(proc_name) => { + if proc_name != "reference" { + panic!( + "/datum/test::reference() did not eval reference() as the ref to read" + ) + } + }, + _ => panic!("/datum/test::reference() failed to eval :: as a proc reference"), + } + // /datum/explicit::static_var = value + let Statement::Expr(static_set) = work_code.get(4).unwrap() else { + panic!("Fifth statement was not an expression") + }; + + let Expression::AssignOp { op: _, lhs, rhs: _ } = static_set else { + panic!("/datum/test::stat = 2 was NOT evaluated as an assignment expression") + }; + let Expression::Base { + term: _, + ref follow, + } = **lhs + else { + panic!("/datum/test::stat was NOT evaluated as a base expression") + }; + match &follow.first().unwrap().elem { + Follow::StaticField(field) => { + if field != "stat" { + panic!("/datum/test::stat did not eval stat as the var to set") + } + }, + _ => panic!("/datum/test::stat failed to eval :: as a static field"), + } + // ::global_proc() + let Statement::Expr(static_set) = work_code.get(5).unwrap() else { + panic!("Sixth statement was not an expression") + }; + + let Expression::Base { term, follow: _ } = static_set else { + panic!("::extra() was NOT evaluated as a base expression") + }; + match &term.elem { + Term::GlobalCall(function, _) => { + if function != "extra" { + panic!("::extra() did not eval extra as the proc to call") + } + }, + _ => panic!("::extra() failed to eval :: as a global call"), + } + // ::global_var + let Statement::Expr(proc_ref) = work_code.get(6).unwrap() else { + panic!("Seventh statement was not an expression") + }; + + let Expression::AssignOp { op: _, lhs: _, rhs } = proc_ref else { + panic!("x = ::bill was NOT evaluated as an assignment expression") + }; + let Expression::Base { + ref term, + follow: _, + } = **rhs + else { + panic!("::bill was NOT evaluated as a base expression") + }; + match &term.elem { + Term::GlobalIdent(field) => { + if field != "bill" { + panic!("::bill did not eval bill as the global var to read") + } + }, + _ => panic!("::bill failed to eval :: as a global var read"), + } + }, + ) +} + +#[test] +fn return_type_union() { + // https://github.com/SpaceManiac/SpacemanDMM/issues/385 + with_code( + " +/proc/returns_num_or_text() as num | text + ", + |context, tree| { + context.assert_success(); + + assert_eq!( + tree.root() + .get_proc("returns_num_or_text") + .unwrap() + .get_declaration() + .unwrap() + .return_type, + ProcReturnType::InputType(InputType::NUM | InputType::TEXT), + ); + }, + ); +} + +#[test] +fn return_type_list() { + // https://github.com/SpaceManiac/SpacemanDMM/issues/399 + with_code( + " +/proc/returns_list() as list + ", + |context, tree| { + context.assert_success(); + + assert_eq!( + tree.root() + .get_proc("returns_list") + .unwrap() + .get_declaration() + .unwrap() + .return_type, + ProcReturnType::InputType(InputType::LIST), + ); + }, + ); +} + +#[test] +fn proc_operator_slash() { + // https://github.com/SpaceManiac/SpacemanDMM/issues/399 + with_code( + " +/datum/operator/proc/foo() +/datum/operator/() +/datum/operator/=() +/datum/operator + proc/bar() + ", + |context, tree| { + context.assert_success(); + + eprintln!("{:#?}", tree.expect("/datum").procs); + + tree.expect("/datum/operator").get_proc("foo").unwrap(); + tree.expect("/datum").get_proc("operator/").unwrap(); + tree.expect("/datum").get_proc("operator/=").unwrap(); + tree.expect("/datum/operator").get_proc("bar").unwrap(); + }, + ); +} + +#[test] +fn for_key_value_with_as() { + // https://github.com/SpaceManiac/SpacemanDMM/issues/421 - 'as' keyword in for-key-value loops + with_code( + " +/proc/test() + var/alist/foo = list() + for (var/index as num, C in foo) + world << index + ", + |context, tree| { + context.assert_success(); + + let proc = tree.root().get_proc("test").unwrap(); + let code = proc.get().code.as_ref().unwrap(); + + if let Statement::ForKeyValue(ref fkv) = code[1].elem { + assert!(fkv.key_input_type.is_some()); + assert_eq!(fkv.key_input_type.unwrap(), InputType::NUM); + } else { + panic!("Expected ForKeyValue statement, got {:?}", code[1].elem); + } + }, + ); } diff --git a/crates/dreammaker/tests/constants_tests.rs b/crates/dreammaker/tests/constants_tests.rs index 130436e0..152ea72f 100644 --- a/crates/dreammaker/tests/constants_tests.rs +++ b/crates/dreammaker/tests/constants_tests.rs @@ -17,10 +17,7 @@ fn floating_point_rgb() { #[test] fn rgb_base() { - assert_eq!( - eval("rgb(0, 255, 0)").unwrap(), - Constant::string("#00ff00"), - ); + assert_eq!(eval("rgb(0, 255, 0)").unwrap(), Constant::string("#00ff00"),); assert_eq!( eval("rgb(50, 50, 50)").unwrap(), Constant::string("#323232"), @@ -93,7 +90,6 @@ fn rgb_hsl() { ); } - #[test] fn rgb_hcy() { assert_eq!( @@ -110,3 +106,11 @@ fn rgb_hcy() { Constant::string("#000000"), ); } + +#[test] +fn no_fexists_outside_preproc() { + assert_eq!( + eval("fexists()").unwrap_err().description(), + "non-constant function call: fexists", + ); +} diff --git a/crates/dreammaker/tests/expression_tests.rs b/crates/dreammaker/tests/expression_tests.rs index 727b340d..0fe05ecf 100644 --- a/crates/dreammaker/tests/expression_tests.rs +++ b/crates/dreammaker/tests/expression_tests.rs @@ -1,13 +1,14 @@ extern crate dreammaker as dm; +use dm::ast::*; use dm::lexer::Lexer; use dm::parser::*; -use dm::ast::*; fn parse_expr(f: &str) -> Expression { let context = Default::default(); let lexer = Lexer::new(&context, Default::default(), f.as_bytes()); - let result = parse_expression(&context, Default::default(), lexer).expect("failed to parse expression"); + let result = + parse_expression(&context, Default::default(), lexer).expect("failed to parse expression"); context.assert_success(); result } @@ -114,3 +115,82 @@ fn bitop_precedence() { } ); } + +#[test] +fn pointer_ops() { + assert_eq!( + parse_expr("*&1"), + Expression::Base { + term: Box::new(Spanned::new(Default::default(), Term::Int(1))), + follow: vec![ + Spanned::new(Default::default(), Follow::Unary(UnaryOp::Reference)), + Spanned::new(Default::default(), Follow::Unary(UnaryOp::Dereference)), + ] + .into_boxed_slice(), + } + ) +} + +#[test] +fn call_ext() { + assert_eq!( + parse_expr("call_ext(\"cat.dll\", \"meow\")(1, 2, 3)"), + Expression::Base { + term: Box::new(Spanned::new( + Default::default(), + Term::ExternalCall { + library: Some(Box::new(Expression::from(Term::String( + "cat.dll".to_owned() + )))), + function: Box::new(Expression::from(Term::String("meow".to_owned()))), + args: Box::new([ + Expression::Base { + term: Box::new(Spanned::new(Default::default(), Term::Int(1))), + follow: Box::new([]) + }, + Expression::Base { + term: Box::new(Spanned::new(Default::default(), Term::Int(2))), + follow: Box::new([]) + }, + Expression::Base { + term: Box::new(Spanned::new(Default::default(), Term::Int(3))), + follow: Box::new([]) + } + ]) + } + )), + follow: Box::new([]), + } + ) +} + +#[test] +fn loaded_call_ext() { + assert_eq!( + parse_expr("call_ext(loaded_cat_meow)(1, 2, 3)"), + Expression::Base { + term: Box::new(Spanned::new( + Default::default(), + Term::ExternalCall { + library: None, + function: Box::new(Expression::from(Term::Ident("loaded_cat_meow".to_owned()))), + args: Box::new([ + Expression::Base { + term: Box::new(Spanned::new(Default::default(), Term::Int(1))), + follow: Box::new([]) + }, + Expression::Base { + term: Box::new(Spanned::new(Default::default(), Term::Int(2))), + follow: Box::new([]) + }, + Expression::Base { + term: Box::new(Spanned::new(Default::default(), Term::Int(3))), + follow: Box::new([]) + } + ]) + } + )), + follow: Box::new([]), + } + ) +} diff --git a/crates/dreammaker/tests/format_tests.rs b/crates/dreammaker/tests/format_tests.rs index 8fcf4b9c..a16511ac 100644 --- a/crates/dreammaker/tests/format_tests.rs +++ b/crates/dreammaker/tests/format_tests.rs @@ -1,6 +1,6 @@ extern crate dreammaker as dm; -use dm::lexer::{Quote, FormatFloat}; +use dm::lexer::{FormatFloat, Quote}; #[test] fn strings() { @@ -16,7 +16,7 @@ fn floats() { assert_eq!(FormatFloat(99999.01).to_string(), "99999"); assert_eq!(FormatFloat(999999.0).to_string(), "999999"); assert_eq!(FormatFloat(5.0e6).to_string(), "5e+006"); - assert_eq!(FormatFloat(5000000i32 as f32).to_string(), "5e+006"); + assert_eq!(FormatFloat(5000000_f32).to_string(), "5e+006"); assert_eq!(FormatFloat(9999999.0).to_string(), "1e+007"); assert_eq!(FormatFloat(9999991.0).to_string(), "9.99999e+006"); @@ -26,8 +26,8 @@ fn floats() { assert_eq!(FormatFloat(0.000500001).to_string(), "0.000500001"); assert_eq!(FormatFloat(0.0000500001).to_string(), "5.00001e-005"); - assert_eq!(FormatFloat(std::f32::INFINITY).to_string(), "1.#INF"); - assert_eq!(FormatFloat(-std::f32::INFINITY).to_string(), "-1.#INF"); + assert_eq!(FormatFloat(f32::INFINITY).to_string(), "1.#INF"); + assert_eq!(FormatFloat(-f32::INFINITY).to_string(), "-1.#INF"); assert_eq!(FormatFloat(2.9).to_string(), "2.9"); assert_eq!(FormatFloat(2.4).to_string(), "2.4"); @@ -40,12 +40,26 @@ fn floats() { fn lists() { use dm::constants::Constant::{self, *}; - assert_eq!(List(vec![ - (Constant::string("KNOCKDOWN"), Some(Float(0.))), - (Constant::string("THROW"), Some(Float(0.))), - ].into()).to_string(), r#"list("KNOCKDOWN" = 0, "THROW" = 0)"#); - assert_eq!(List(vec![ - (Constant::string("neutral"), None), - (Constant::string("Syndicate"), None), - ].into()).to_string(), r#"list("neutral","Syndicate")"#); + assert_eq!( + List( + vec![ + (Constant::string("KNOCKDOWN"), Some(Float(0.))), + (Constant::string("THROW"), Some(Float(0.))), + ] + .into() + ) + .to_string(), + r#"list("KNOCKDOWN" = 0, "THROW" = 0)"# + ); + assert_eq!( + List( + vec![ + (Constant::string("neutral"), None), + (Constant::string("Syndicate"), None), + ] + .into() + ) + .to_string(), + r#"list("neutral","Syndicate")"# + ); } diff --git a/crates/dreammaker/tests/lexer_tests.rs b/crates/dreammaker/tests/lexer_tests.rs index 59232298..6e947205 100644 --- a/crates/dreammaker/tests/lexer_tests.rs +++ b/crates/dreammaker/tests/lexer_tests.rs @@ -1,8 +1,8 @@ extern crate dreammaker as dm; -use dm::lexer::*; -use dm::lexer::Token::*; use dm::lexer::Punctuation::*; +use dm::lexer::Token::*; +use dm::lexer::*; fn lex(f: &str) -> Vec { let context = Default::default(); @@ -15,7 +15,7 @@ fn lex(f: &str) -> Vec { fn one_token(f: &str) -> Token { let mut v = lex(f); - assert_eq!(v.len(), 2, "not one token: {:?} -> {:?}", f, v); + assert_eq!(v.len(), 2, "not one token: {f:?} -> {v:?}"); assert_eq!(v[1], Punct(Newline)); v.remove(0) } @@ -23,7 +23,7 @@ fn one_token(f: &str) -> Token { fn float(f: &str) -> f32 { match one_token(f) { Token::Float(f) => f, - other => panic!("{:?}: expected float, got {:?}", f, other), + other => panic!("{f:?}: expected float, got {other:?}"), } } @@ -33,10 +33,14 @@ fn number_literals() { assert_eq!(lex("0xABCDE"), vec![Int(703710), Punct(Newline)]); assert_eq!(lex("1e4"), vec![Float(10000.0), Punct(Newline)]); - let f = float("1.#INF"); assert!(f.is_infinite() && f > 0.); - let f = float("1.#IND"); assert!(f.is_nan()); - let f = float("1#INF"); assert!(f.is_infinite() && f > 0.); - let f = float("1#IND"); assert!(f.is_nan()); + let f = float("1.#INF"); + assert!(f.is_infinite() && f > 0.); + let f = float("1.#IND"); + assert!(f.is_nan()); + let f = float("1#INF"); + assert!(f.is_infinite() && f > 0.); + let f = float("1#IND"); + assert!(f.is_nan()); } #[test] @@ -59,16 +63,13 @@ fn empty_block_comment() { // This is legal. It should not do either of the following: // - Error with "still skipping comments at end of file" // - Yield a DocComment { text: "", .. } - assert_eq!( - lex(r#"/**/"#), - vec![Punct(Newline)] - ) + assert_eq!(lex(r#"/**/"#), vec![Punct(Newline)]) } #[test] fn raw_strings() { let desired = Token::String("content".to_owned()); - let stuff = lex(r###" + let stuff = lex(r#" @"content" @xcontentx @/content/ @@ -77,9 +78,67 @@ fn raw_strings() { @(very long terminator)contentvery long terminator @{"content"} @{content{ -"###); +"#); for each in stuff.iter() { - if each == &Punct(Newline) { continue } + if each == &Punct(Newline) { + continue; + } assert_eq!(each, &desired); } } + +#[test] +fn heredoc_with_quotes() { + // 1-3 quotes in the middle of ordinary characters + assert_eq!( + lex(r#"{"foo"bar"}"#), + vec![ + Token::String(r#"foo"bar"#.to_owned()), + Token::Punct(Punctuation::Newline), + ] + ); + assert_eq!( + lex(r#"{"foo""bar"}"#), + vec![ + Token::String(r#"foo""bar"#.to_owned()), + Token::Punct(Punctuation::Newline), + ] + ); + assert_eq!( + lex(r#"{"foo"""bar"}"#), + vec![ + Token::String(r#"foo"""bar"#.to_owned()), + Token::Punct(Punctuation::Newline), + ] + ); + + // 0-5 quotes at the start/end + assert_eq!( + lex(r#"{""}"#), + vec![ + Token::String(r#""#.to_owned()), + Token::Punct(Punctuation::Newline), + ] + ); + assert_eq!( + lex(r#"{"""}"#), + vec![ + Token::String(r#"""#.to_owned()), + Token::Punct(Punctuation::Newline), + ] + ); + assert_eq!( + lex(r#"{""""}"#), + vec![ + Token::String(r#""""#.to_owned()), + Token::Punct(Punctuation::Newline), + ] + ); + assert_eq!( + lex(r#"{"""""}"#), + vec![ + Token::String(r#"""""#.to_owned()), + Token::Punct(Punctuation::Newline), + ] + ); +} diff --git a/crates/dreammaker/tests/location_tests.rs b/crates/dreammaker/tests/location_tests.rs index 912da1b6..d979369b 100644 --- a/crates/dreammaker/tests/location_tests.rs +++ b/crates/dreammaker/tests/location_tests.rs @@ -4,7 +4,7 @@ use dm::lexer::*; #[test] fn simple_location_test() { - let code = r##" + let code = r#" #define islist(thing) istype(thing, /list) /datum/globals @@ -15,10 +15,12 @@ fn simple_location_test() { world.log << new/ obj() /var/foo = bar -"##.trim(); +"# + .trim(); let context = Default::default(); - let located_tokens: Vec<_> = Lexer::new(&context, Default::default(), code.as_bytes()).collect(); + let located_tokens: Vec<_> = + Lexer::new(&context, Default::default(), code.as_bytes()).collect(); context.assert_success(); assert_eq!(located_tokens[0].location.line, 1); @@ -26,23 +28,30 @@ fn simple_location_test() { println!("---- lexer ----"); for token in located_tokens.iter() { - println!("{}:{}: {:?}", token.location.line, token.location.column, token.token); + println!( + "{}:{}: {:?}", + token.location.line, token.location.column, token.token + ); } let reconstructed = reconstruct(&located_tokens, false); if reconstructed.trim() != code { - println!("{}", reconstructed); + println!("{reconstructed}"); panic!("Some lines differed"); } println!("---- indent processor ----"); - let indented_tokens: Vec<_> = dm::indents::IndentProcessor::new(&context, located_tokens).collect(); + let indented_tokens: Vec<_> = + dm::indents::IndentProcessor::new(&context, located_tokens).collect(); context.assert_success(); for token in indented_tokens.iter() { - println!("{}:{}: {:?}", token.location.line, token.location.column, token.token); + println!( + "{}:{}: {:?}", + token.location.line, token.location.column, token.token + ); } let reconstructed = reconstruct(&indented_tokens, true); - println!("{}", reconstructed); + println!("{reconstructed}"); } fn reconstruct(tokens: &[LocatedToken], iffy: bool) -> String { @@ -74,7 +83,7 @@ fn reconstruct(tokens: &[LocatedToken], iffy: bool) -> String { write!(this_line, "{}", token.token).unwrap(); } for each in reconstructed.iter_mut() { - if !each.ends_with("\n") { + if !each.ends_with('\n') { each.push('\n'); } } diff --git a/crates/dreammaker/tests/macro_tests.rs b/crates/dreammaker/tests/macro_tests.rs index afefbe84..08f34b63 100644 --- a/crates/dreammaker/tests/macro_tests.rs +++ b/crates/dreammaker/tests/macro_tests.rs @@ -1,15 +1,16 @@ extern crate dreammaker as dm; -use dm::preprocessor::*; -use dm::lexer::Token::*; use dm::lexer::Punctuation::*; +use dm::lexer::Token::*; +use dm::preprocessor::*; fn process(source: &'static str) -> Vec { let ctx = dm::Context::default(); let pp = Preprocessor::from_buffer(&ctx, "macro_tests.rs".into(), source); // collect tokens, strip leading and trailing newlines - let mut tokens: Vec<_> = pp.map(|loctok| loctok.token) + let mut tokens: Vec<_> = pp + .map(|loctok| loctok.token) .skip_while(|tok| *tok == Punct(Newline)) .collect(); ctx.assert_success(); @@ -22,50 +23,86 @@ fn process(source: &'static str) -> Vec { #[test] fn clamp_inside_clamp() { // check that both the inner and outer "CLAMP" calls became "clamp" calls - assert_eq!(process(r#" + assert_eq!( + process( + r#" #define CLAMP(VAL, MIN, MAX) clamp(VAL, MIN, MAX) CLAMP(alpha - CLAMP(beta - 2, 0, beta), 3, alpha) -"#), &[ - Ident("clamp".into(), false), - Punct(LParen), +"# + ), + &[ + Ident("clamp".into(), false), + Punct(LParen), Ident("alpha".into(), true), Punct(Sub), Ident("clamp".into(), false), Punct(LParen), - Ident("beta".into(), true), - Punct(Sub), - Int(2), - Punct(Comma), - Int(0), - Punct(Comma), - Ident("beta".into(), false), + Ident("beta".into(), true), + Punct(Sub), + Int(2), + Punct(Comma), + Int(0), + Punct(Comma), + Ident("beta".into(), false), Punct(RParen), Punct(Comma), Int(3), Punct(Comma), Ident("alpha".into(), false), - Punct(RParen), - ]); + Punct(RParen), + ] + ); } #[test] fn defined_function() { - assert_eq!(process(r#" + assert_eq!( + process( + r#" #define FOO #if defined(FOO) ok1 #endif -"#), &[ - Ident("ok1".into(), false), - ]); +"# + ), + &[Ident("ok1".into(), false),] + ); - assert_eq!(process(r#" + assert_eq!( + process( + r#" #define A multiple.tokens() #if defined(C) || defined(A) && !defined(B) ok2 #endif -"#), &[ - Ident("ok2".into(), false), - ]); +"# + ), + &[Ident("ok2".into(), false),] + ); +} + +#[test] +fn fexists_function() { + assert_eq!( + process( + r#" +#if fexists("README.md") +exists +#endif +"# + ), + &[Ident("exists".into(), false),] + ); + + assert_eq!( + process( + r#" +#if fexists("this file does not exist") +exists +#endif +"# + ), + &[] + ); } diff --git a/crates/dreammaker/tests/pipeline_tests.rs b/crates/dreammaker/tests/pipeline_tests.rs index 7903c05c..e99fb1e2 100644 --- a/crates/dreammaker/tests/pipeline_tests.rs +++ b/crates/dreammaker/tests/pipeline_tests.rs @@ -2,8 +2,8 @@ extern crate dreammaker as dm; use std::path::PathBuf; -use dm::*; use dm::preprocessor::Preprocessor; +use dm::*; fn with_test_dme(context: &Context, f: F) { let dme = match std::env::var_os("TEST_DME") { @@ -11,7 +11,7 @@ fn with_test_dme(context: &Context, f: F) { None => { println!("Set TEST_DME to check full pipeline"); return; - } + }, }; f(Preprocessor::new(context, PathBuf::from(dme)).expect("failed to open test file")) } @@ -35,7 +35,8 @@ fn check_indentor() { &mut string, indents::IndentProcessor::new(&context, &mut preprocessor).map(|t| t.token), true, - ).unwrap(); + ) + .unwrap(); context.assert_success(); }); } @@ -44,7 +45,10 @@ fn check_indentor() { fn check_parser() { let context = Context::default(); with_test_dme(&context, |mut preprocessor| { - let mut parser = parser::Parser::new(&context, indents::IndentProcessor::new(&context, &mut preprocessor)); + let mut parser = parser::Parser::new( + &context, + indents::IndentProcessor::new(&context, &mut preprocessor), + ); parser.enable_procs(); let _tree = parser.parse_object_tree(); context.assert_success(); diff --git a/crates/interval-tree/Cargo.toml b/crates/interval-tree/Cargo.toml index 66b430eb..57c71494 100644 --- a/crates/interval-tree/Cargo.toml +++ b/crates/interval-tree/Cargo.toml @@ -3,10 +3,10 @@ name = "interval-tree" version = "0.8.0" authors = ["coco ", "Tad Hardesty "] description = "A simple Interval Tree implementation" -repository = "https://github.com/SpaceManiac/SpacemanDMM/tree/master/src/interval_tree" +repository = "https://github.com/SpaceManiac/SpacemanDMM/tree/master/crates/interval-tree" keywords = ["datastructure", "interval", "tree", "map"] license-file = "LICENSE" -edition = "2018" +edition = "2021" [dev-dependencies] -rand = "0.8.4" +rand = "0.8.5" diff --git a/crates/interval-tree/src/iterators.rs b/crates/interval-tree/src/iterators.rs index 2a3f9cda..f1c9682a 100644 --- a/crates/interval-tree/src/iterators.rs +++ b/crates/interval-tree/src/iterators.rs @@ -1,7 +1,7 @@ +use crate::node::Node; +use crate::{IntervalTree, RangeInclusive}; use std::collections::Bound; use std::vec; -use crate::{IntervalTree, RangeInclusive}; -use crate::node::Node; #[derive(Debug, Clone)] enum Visiting { @@ -32,7 +32,8 @@ impl<'a, K: Ord, V> RangePairIter<'a, K, V> { match node.left { Some(ref lsucc) => { self.stack.push((node, Visiting::Center)); - if match self.start { // left_subtree_relevant + if match self.start { + // left_subtree_relevant Bound::Included(ref start) => node.max >= *start, Bound::Excluded(ref start) => node.max > *start, Bound::Unbounded => true, @@ -40,16 +41,19 @@ impl<'a, K: Ord, V> RangePairIter<'a, K, V> { self.stack.push((&**lsucc, Visiting::Left)) } }, - None => self.stack.push((node, Visiting::Center)) + None => self.stack.push((node, Visiting::Center)), } } fn visit_right(&mut self, node: &'a Node) { - if !match self.end { // right_subtree_relevant + if !match self.end { + // right_subtree_relevant Bound::Included(ref end) => *end >= node.key.start, Bound::Excluded(ref end) => *end > node.key.start, Bound::Unbounded => true, - } { return } + } { + return; + } if let Some(ref rsucc) = node.right { self.stack.push((&**rsucc, Visiting::Left)); } @@ -76,7 +80,7 @@ impl<'a, K: Ord, V> RangePairIter<'a, K, V> { self.stack.push((node, Visiting::Middle(i + 1))); return Some((node, i)); } - } + }, } } None @@ -87,7 +91,8 @@ impl<'a, K: Ord + Clone, V> Iterator for RangePairIter<'a, K, V> { type Item = (RangeInclusive, &'a V); fn next(&mut self) -> Option<(RangeInclusive, &'a V)> { - self.get_next_node().map(|(n, i)| (n.key.clone(), &n.data[i])) + self.get_next_node() + .map(|(n, i)| (n.key.clone(), &n.data[i])) } } @@ -149,10 +154,7 @@ impl Iterator for IntoIter { } if self.current.is_none() { - let mut node = match self.stack.pop() { - Some(node) => node, - None => return None, - }; + let mut node = self.stack.pop()?; self.push_node(node.right.take()); self.current = Some((node.key, node.data.into_iter())); } @@ -163,55 +165,88 @@ impl Iterator for IntoIter { #[test] fn test_iterators() { let mut tree = IntervalTree::::new(); - tree.insert(RangeInclusive::new(18,18), 1337); - tree.insert(RangeInclusive::new(13,13), 1338); - tree.insert(RangeInclusive::new(17,17), 1339); - tree.insert(RangeInclusive::new(10,10), 1321); - tree.insert(RangeInclusive::new(1 ,1), 1321); - tree.insert(RangeInclusive::new(3 ,3), 1322); + tree.insert(RangeInclusive::new(18, 18), 1337); + tree.insert(RangeInclusive::new(13, 13), 1338); + tree.insert(RangeInclusive::new(17, 17), 1339); + tree.insert(RangeInclusive::new(10, 10), 1321); + tree.insert(RangeInclusive::new(1, 1), 1321); + tree.insert(RangeInclusive::new(3, 3), 1322); let iter = RangePairIter::new(&tree, Bound::Included(0), Bound::Included(1000)); - for (k,v) in iter { - println!("{:?} {}",k,v); + for (k, v) in iter { + println!("{k:?} {v}"); } let mut iter = RangePairIter::new(&tree, Bound::Included(0), Bound::Included(1000)); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(1 ,1 )); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(3 ,3 )); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(10,10)); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(13,13)); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(17,17)); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(18,18)); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(1, 1) + ); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(3, 3) + ); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(10, 10) + ); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(13, 13) + ); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(17, 17) + ); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(18, 18) + ); assert!(iter.next().is_none()); let mut iter = RangePairIter::new(&tree, Bound::Included(3), Bound::Included(17)); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(3 ,3 )); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(10,10)); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(13,13)); - assert_eq!(iter.next().expect("should have a few values").0, RangeInclusive::new(17,17)); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(3, 3) + ); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(10, 10) + ); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(13, 13) + ); + assert_eq!( + iter.next().expect("should have a few values").0, + RangeInclusive::new(17, 17) + ); assert!(iter.next().is_none()); } #[test] fn test_into_iter() { let mut tree = IntervalTree::::new(); - tree.insert(RangeInclusive::new(18,18), 1337); - tree.insert(RangeInclusive::new(13,13), 1338); - tree.insert(RangeInclusive::new(17,17), 1339); - tree.insert(RangeInclusive::new(10,10), 1321); - tree.insert(RangeInclusive::new(1 ,1), 1321); - tree.insert(RangeInclusive::new(1 ,1), 1350); - tree.insert(RangeInclusive::new(3 ,3), 1322); + tree.insert(RangeInclusive::new(18, 18), 1337); + tree.insert(RangeInclusive::new(13, 13), 1338); + tree.insert(RangeInclusive::new(17, 17), 1339); + tree.insert(RangeInclusive::new(10, 10), 1321); + tree.insert(RangeInclusive::new(1, 1), 1321); + tree.insert(RangeInclusive::new(1, 1), 1350); + tree.insert(RangeInclusive::new(3, 3), 1322); let values: Vec<_> = tree.into_iter().collect(); - assert_eq!(values, vec![ - (RangeInclusive::new(1, 1), 1321), - (RangeInclusive::new(1, 1), 1350), - (RangeInclusive::new(3, 3), 1322), - (RangeInclusive::new(10, 10), 1321), - (RangeInclusive::new(13, 13), 1338), - (RangeInclusive::new(17, 17), 1339), - (RangeInclusive::new(18, 18), 1337), - ]); + assert_eq!( + values, + vec![ + (RangeInclusive::new(1, 1), 1321), + (RangeInclusive::new(1, 1), 1350), + (RangeInclusive::new(3, 3), 1322), + (RangeInclusive::new(10, 10), 1321), + (RangeInclusive::new(13, 13), 1338), + (RangeInclusive::new(17, 17), 1339), + (RangeInclusive::new(18, 18), 1337), + ] + ); } diff --git a/crates/interval-tree/src/lib.rs b/crates/interval-tree/src/lib.rs index 65239d53..e381f15b 100644 --- a/crates/interval-tree/src/lib.rs +++ b/crates/interval-tree/src/lib.rs @@ -3,14 +3,14 @@ //! [interval tree]: https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree #![forbid(unsafe_code)] -mod range; -mod node; -mod tree; mod iterators; +mod node; +mod range; +mod tree; -pub use range::{RangeInclusive, range}; -pub use tree::IntervalTree; pub use iterators::RangePairIter; +pub use range::{range, RangeInclusive}; +pub use tree::IntervalTree; #[cfg(test)] mod tests; diff --git a/crates/interval-tree/src/node.rs b/crates/interval-tree/src/node.rs index 2b2b7b24..000ea447 100644 --- a/crates/interval-tree/src/node.rs +++ b/crates/interval-tree/src/node.rs @@ -1,5 +1,5 @@ -use std::cmp::{self, Ordering}; use crate::range::RangeInclusive; +use std::cmp::{self, Ordering}; #[derive(Debug, Clone)] pub struct Node { @@ -48,12 +48,16 @@ impl Node { ///returns the minimal key,value pair within this tree pub fn min_pair(&self) -> (&RangeInclusive, &[V]) { - self.left.as_ref().map_or((&self.key, &self.data), |n| n.min_pair()) + self.left + .as_ref() + .map_or((&self.key, &self.data), |n| n.min_pair()) } ///returns the maximal key,value pair within this tree pub fn max_pair(&self) -> (&RangeInclusive, &[V]) { - self.right.as_ref().map_or((&self.key, &self.data), |n| n.max_pair()) + self.right + .as_ref() + .map_or((&self.key, &self.data), |n| n.max_pair()) } /// Perform a single right rotation on this (sub) tree @@ -106,7 +110,7 @@ impl Node { fn rotate_if_necessary(self: Box) -> Box { match self.diff_of_successors_height() { 2 => self.rotate_left_successor(), - 1 | 0 | -1 => self, + -1..=1 => self, -2 => self.rotate_right_successor(), _ => unreachable!(), } @@ -121,10 +125,10 @@ impl Node { //Return a new Interval tree, where the root has been removed fn delete_root(mut self) -> Option> { match (self.left.take(), self.right.take()) { - (None, None) => None, - (Some(l), None) => Some(l), - (None, Some(r)) => Some(r), - (Some(l), Some(r)) => Some(Node::combine_two_subtrees(l, r)) + (None, None) => None, + (Some(l), None) => Some(l), + (None, Some(r)) => Some(r), + (Some(l), Some(r)) => Some(Node::combine_two_subtrees(l, r)), } } @@ -171,7 +175,7 @@ impl Node { self.left = succ.delete(key); return Some(self.updated_node()); } - } + }, } Some(self) } @@ -195,9 +199,12 @@ impl Node { /// root may now differ due to rotations, thus the old root is moved into the function) pub fn insert(mut self: Box, key: RangeInclusive, data: V) -> Box { match self.key.cmp(&key) { - Ordering::Equal => { self.data.push(data); return self }, - Ordering::Less => self.right = Node::insert_in_successor(self.right.take(), key, data), - Ordering::Greater => self.left = Node::insert_in_successor(self.left.take(), key, data) + Ordering::Equal => { + self.data.push(data); + return self; + }, + Ordering::Less => self.right = Node::insert_in_successor(self.right.take(), key, data), + Ordering::Greater => self.left = Node::insert_in_successor(self.left.take(), key, data), } self.update_height(); self.rotate_if_necessary() @@ -205,7 +212,11 @@ impl Node { /// recursively insert the (key,data) pair into the given optional succesor and return its new /// value - fn insert_in_successor(succ: Option>, key: RangeInclusive, data: V) -> Option> { + fn insert_in_successor( + succ: Option>, + key: RangeInclusive, + data: V, + ) -> Option> { Some(match succ { Some(succ) => succ.insert(key, data), None => Box::new(Node::new(key, data)), @@ -219,175 +230,205 @@ pub fn height(node: &Option>>) -> u32 { #[cfg(test)] pub mod tests { -use super::*; -type Node = super::Node; + use super::*; + type Node = super::Node; -/// returns true iff key is stored in the tree given by root -pub fn contains(key: &RangeInclusive, root: &Box>) -> bool { - root.search(key).is_some() -} + /// returns true iff key is stored in the tree given by root + pub fn contains(key: &RangeInclusive, root: &Node) -> bool { + root.search(key).is_some() + } -fn subtree_max(node: &Option>>) -> u64 { - node.as_ref().map_or(0, |succ| succ.max) -} + fn subtree_max(node: &Option>>) -> u64 { + node.as_ref().map_or(0, |succ| succ.max) + } -///returns the smallest key and value after the given key. -pub fn min_after<'a,V>(key: &RangeInclusive, root: &'a Box>) -> Option<(&'a RangeInclusive,&'a [V])> { - match root.key.cmp(key){ - Ordering::Equal => root.right.as_ref().map_or(None, |succ| Some(succ.min_pair())), - Ordering::Less => root.right.as_ref().map_or(None, |succ| min_after(key, succ)), - Ordering::Greater => { - match root.left { - Some(ref succ) => min_after(key, &succ).or( Some((&root.key,&root.data)) ), - None => Some((&root.key, &root.data)) + ///returns the smallest key and value after the given key. + pub fn min_after<'a, V>( + key: &RangeInclusive, + root: &'a Node, + ) -> Option<(&'a RangeInclusive, &'a [V])> { + match root.key.cmp(key) { + Ordering::Equal => root.right.as_ref().map(|succ| succ.min_pair()), + Ordering::Less => root.right.as_ref().and_then(|succ| min_after(key, succ)), + Ordering::Greater => match root.left { + Some(ref succ) => min_after(key, succ).or(Some((&root.key, &root.data))), + None => Some((&root.key, &root.data)), + }, + } + } + + ///returns the minimal value within this tree + pub fn min(root: &Node) -> &[V] { + root.left.as_ref().map_or(&root.data, |v| min(v)) + } + + ///returns the minimal value within this tree + pub fn max(root: &Node) -> &[V] { + root.right.as_ref().map_or(&root.data, |v| max(v)) + } + + fn simple_tree(size: i32) -> Box> { + let mut t = Box::new(Node:: { + key: RangeInclusive::new(1, 1), + data: vec![1337], + height: 0, + max: 1, + left: None, + right: None, + }); + for x in 2..size + 1 { + t = t.insert(RangeInclusive::new(x as u64, x as u64), 1337 + x - 1) + } + t + } + fn is_sorted_left(node: &Node) -> bool { + node.left.as_ref().is_none_or(|succ| succ.key < node.key) + } + fn is_sorted_right(node: &Node) -> bool { + node.right.as_ref().is_none_or(|succ| succ.key > node.key) + } + fn is_interval_node(node: &Node) -> bool { + let sorted = is_sorted_left(node) && is_sorted_right(node); + let balanced = node.height == cmp::max(height(&node.left), height(&node.right)) + 1; + let proper_max = node.max + == cmp::max( + subtree_max(&node.left), + cmp::max(subtree_max(&node.right), node.key.end), + ); + sorted && balanced && proper_max + } + + pub fn is_interval_tree(root: &Option>>) -> bool { + (*root).as_ref().is_none_or(|n| is_interval_node(n)) + } + + #[test] + fn simple_tree_operations() { + let mut t = Box::new(Node:: { + key: RangeInclusive::new(3, 3), + data: vec![4], + max: 3, + height: 2, + left: Some(Box::new(Node:: { + key: RangeInclusive::new(2, 2), + data: vec![5], + height: 1, + max: 2, + left: None, + right: None, + })), + right: None, + }); + assert!(is_interval_node(&t)); + assert!(contains::(&RangeInclusive::new(3, 3), &t)); + assert!(contains::(&RangeInclusive::new(2, 2), &t)); + assert!(!contains::(&RangeInclusive::new(6, 6), &t)); + assert!(!contains::(&RangeInclusive::new(4, 4), &t)); + t = t.insert(RangeInclusive::new(4, 4), 7); + t = t.insert(RangeInclusive::new(5, 5), 7); + t = t.insert(RangeInclusive::new(6, 6), 8); + assert!(contains::(&RangeInclusive::new(4, 4), &t)); + assert!(contains::(&RangeInclusive::new(6, 6), &t)); + assert!(!contains::(&RangeInclusive::new(7, 7), &t)); + } + + #[test] + fn rotations_on_tree() { + let mut t = Box::new(Node:: { + key: RangeInclusive::new(1, 1), + data: vec![1337], + height: 1, + max: 1, + left: None, + right: None, + }); + for i in 2..255 { + t = t.insert(RangeInclusive::new(i, i), 1337); + assert!(is_interval_node(&t)); + } + //check that the tree is indeed balanced + assert!(height(&Some(t)) <= 8); + } + + #[test] + fn test_drop_min() { + let mut t = simple_tree(3); + let (maybe_tree, min) = t.drop_min(); + t = maybe_tree.expect("failure to get tree for first min delete"); + assert!(is_interval_node(&t)); + assert!(min.key == RangeInclusive::new(1, 1)); + assert!(!contains::(&RangeInclusive::new(1, 1), &t)); + assert!(contains::(&RangeInclusive::new(2, 2), &t)); + assert!(contains::(&RangeInclusive::new(3, 3), &t)); + + let (maybe_tree, min) = t.drop_min(); + t = maybe_tree.expect("failure to get tree for second min delete"); + assert!(is_interval_node(&t)); + assert!(min.key == RangeInclusive::new(2, 2)); + assert!(!contains::(&RangeInclusive::new(1, 1), &t)); + assert!(!contains::(&RangeInclusive::new(2, 2), &t)); + assert!(contains::(&RangeInclusive::new(3, 3), &t)); + + let (maybe_tree, min) = t.drop_min(); + assert!(maybe_tree.is_none()); + assert!(min.key == RangeInclusive::new(3, 3)); + } + + #[test] + fn test_drop_root() { + let mut t = simple_tree(3); + let maybe_tree = t.delete_root(); + t = maybe_tree.expect("failure to get tree for first root drop"); + assert!(is_interval_node(&t)); + assert!(t.height == 2); + assert!(contains::(&RangeInclusive::new(1, 1), &t)); + assert!(!contains::(&RangeInclusive::new(2, 2), &t)); + assert!(contains::(&RangeInclusive::new(3, 3), &t)); + + let maybe_tree = t.delete_root(); + t = maybe_tree.expect("failure to get tree for second root drop"); + assert!(is_interval_node(&t)); + assert!(contains::(&RangeInclusive::new(1, 1), &t)); + assert!(!contains::(&RangeInclusive::new(2, 2), &t)); + assert!(!contains::(&RangeInclusive::new(3, 3), &t)); + + let maybe_tree = t.delete_root(); + assert!(maybe_tree.is_none()); + } + + #[test] + fn test_delete() { + let mut t = simple_tree(10); + for i in 1..10 { + assert!(contains::(&RangeInclusive::new(i, i), &t)); + let maybe_tree = t.delete(RangeInclusive::new(i, i)); + t = maybe_tree.expect("failure to get tree for delete"); + assert!(!contains::(&RangeInclusive::new(i, i), &t)); + assert!(is_interval_node(&t)); + } + assert!(contains::(&RangeInclusive::new(10, 10), &t)); + let maybe_tree = t.delete(RangeInclusive::new(10, 10)); + assert!(maybe_tree.is_none()); + } + + #[test] + fn test_min_max() { + let t = simple_tree(50); + assert_eq!(min(&t), &[1337]); + assert_eq!(max(&t), &[1337 + 50 - 1]); + assert_eq!(t.max_pair().0, &RangeInclusive::new(50, 50)); + assert_eq!(t.min_pair().0, &RangeInclusive::new(1, 1)); + } + + #[test] + fn test_min_after() { + let t = simple_tree(50); + for old_key in 0..55 { + println!("trying value: {old_key}"); + match min_after(&RangeInclusive::new(old_key, old_key), &t) { + Some((k, _d)) => assert_eq!(k, &(RangeInclusive::new(old_key + 1, old_key + 1))), + None => assert!(old_key >= 50), } } } } - -///returns the minimal value within this tree -pub fn min(root: &Box>) -> &[V] { - root.left.as_ref().map_or(&root.data, min) -} - -///returns the minimal value within this tree -pub fn max(root: &Box>) -> &[V] { - root.right.as_ref().map_or(&root.data, max) -} - - -fn simple_tree(size: i32) -> Box> { - let mut t = Box::new(Node::{key: RangeInclusive::new(1,1), data: vec![1337], height: 0, max: 1, left:None, right: None}); - for x in 2..size+1 { - t = t.insert(RangeInclusive::new(x as u64, x as u64 ),1337+x-1) - } - t -} -fn is_sorted_left(node: &Box>) -> bool { - node.left.as_ref().map_or(true, |succ| succ.key < node.key) -} -fn is_sorted_right(node: &Box>) -> bool { - node.right.as_ref().map_or(true, |succ| succ.key > node.key) -} -fn is_interval_node(node: &Box>) -> bool { - let sorted = is_sorted_left(node) && is_sorted_right(node); - let balanced = node.height == cmp::max(height(&node.left),height(&node.right))+1; - let proper_max = node.max == cmp::max(subtree_max(&node.left), cmp::max(subtree_max(&node.right), node.key.end)); - return sorted && balanced && proper_max; -} - -pub fn is_interval_tree(root: &Option>>) -> bool { - (*root).as_ref().map_or(true, is_interval_node) -} - -#[test] -fn simple_tree_operations() { - let mut t = Box::new(Node::{key: RangeInclusive::new(3,3), data: vec![4], max:3, height: 2, - left: Some(Box::new(Node::{key: RangeInclusive::new(2,2), data: vec![5], height:1, max: 2, left: None, right: None})), - right: None}); - assert!(is_interval_node(&t)); - assert!( contains::(&RangeInclusive::new(3,3),&t) ); - assert!( contains::(&RangeInclusive::new(2,2),&t) ); - assert!( !contains::(&RangeInclusive::new(6,6),&t) ); - assert!( !contains::(&RangeInclusive::new(4,4),&t) ); - t = t.insert(RangeInclusive::new(4,4),7); - t = t.insert(RangeInclusive::new(5,5),7); - t = t.insert(RangeInclusive::new(6,6),8); - assert!( contains::(&RangeInclusive::new(4,4),&t) ); - assert!( contains::(&RangeInclusive::new(6,6),&t) ); - assert!( !contains::(&RangeInclusive::new(7,7),&t) ); -} - -#[test] -fn rotations_on_tree(){ - let mut t = Box::new(Node::{key: RangeInclusive::new(1,1), data: vec![1337], height: 1, max: 1, left: None, right: None}); - for i in 2..255 { - t = t.insert(RangeInclusive::new(i,i),1337); - assert!(is_interval_node(&t)); - } - //check that the tree is indeed balanced - assert!(height(&Some(t)) <= 8); -} - -#[test] -fn test_drop_min(){ - let mut t = simple_tree(3); - let (maybe_tree,min) = t.drop_min(); - t = maybe_tree.expect("failure to get tree for first min delete"); - assert!(is_interval_node(&t)); - assert!( min.key == RangeInclusive::new(1,1)); - assert!(!contains::(&RangeInclusive::new(1,1),&t)); - assert!( contains::(&RangeInclusive::new(2,2),&t)); - assert!( contains::(&RangeInclusive::new(3,3),&t)); - - let (maybe_tree,min) = t.drop_min(); - t = maybe_tree.expect("failure to get tree for second min delete"); - assert!(is_interval_node(&t)); - assert!( min.key == RangeInclusive::new(2,2)); - assert!(!contains::(&RangeInclusive::new(1,1),&t)); - assert!(!contains::(&RangeInclusive::new(2,2),&t)); - assert!( contains::(&RangeInclusive::new(3,3),&t)); - - let (maybe_tree,min) = t.drop_min(); - assert!( maybe_tree.is_none() ); - assert!( min.key == RangeInclusive::new(3,3)); -} - -#[test] -fn test_drop_root(){ - let mut t = simple_tree(3); - let maybe_tree = t.delete_root(); - t = maybe_tree.expect("failure to get tree for first root drop"); - assert!(is_interval_node(&t)); - assert!( t.height == 2); - assert!( contains::(&RangeInclusive::new(1,1),&t)); - assert!(!contains::(&RangeInclusive::new(2,2),&t)); - assert!( contains::(&RangeInclusive::new(3,3),&t)); - - let maybe_tree = t.delete_root(); - t = maybe_tree.expect("failure to get tree for second root drop"); - assert!(is_interval_node(&t)); - assert!( contains::(&RangeInclusive::new(1,1),&t)); - assert!(!contains::(&RangeInclusive::new(2,2),&t)); - assert!(!contains::(&RangeInclusive::new(3,3),&t)); - - let maybe_tree = t.delete_root(); - assert!( maybe_tree.is_none() ); -} - -#[test] -fn test_delete(){ - let mut t = simple_tree(10); - for i in 1..10 { - assert!(contains::(&RangeInclusive::new(i,i),&t)); - let maybe_tree = t.delete(RangeInclusive::new(i,i)); - t = maybe_tree.expect("failure to get tree for delete"); - assert!(!contains::(&RangeInclusive::new(i,i),&t)); - assert!(is_interval_node(&t)); - } - assert!(contains::(&RangeInclusive::new(10,10),&t)); - let maybe_tree = t.delete(RangeInclusive::new(10,10)); - assert!(maybe_tree.is_none()); -} - -#[test] -fn test_min_max() { - let t = simple_tree(50); - assert_eq!(min(&t),&[1337]); - assert_eq!(max(&t),&[1337+50-1]); - assert_eq!(t.max_pair().0,&RangeInclusive::new(50,50)); - assert_eq!(t.min_pair().0,&RangeInclusive::new(1,1)); -} - -#[test] -fn test_min_after(){ - let t = simple_tree(50); - for old_key in 0..55 { - println!("trying value: {}", old_key); - match min_after(&RangeInclusive::new(old_key,old_key),&t) { - Some((k,_d)) => assert_eq!(k, &(RangeInclusive::new(old_key+1,old_key+1))), - None => assert!(old_key >= 50) - } - } -} -} diff --git a/crates/interval-tree/src/tests.rs b/crates/interval-tree/src/tests.rs index fa372f7c..aece4772 100644 --- a/crates/interval-tree/src/tests.rs +++ b/crates/interval-tree/src/tests.rs @@ -1,149 +1,154 @@ +use crate::RangeInclusive; use std::cmp; use std::collections::BTreeSet; -use crate::RangeInclusive; type IntervalTree = crate::IntervalTree; #[test] -fn test_getters(){ +fn test_getters() { let data = 1337; let mut t = IntervalTree::::new(); - t.insert(RangeInclusive::new(1,1), data); - t.insert(RangeInclusive::new(2,2), data+1); - t.insert(RangeInclusive::new(3,3), data+2); - assert_eq!(t.get_or(RangeInclusive::new(1,1), &[0]), &[data]); - assert_eq!(t.get_or(RangeInclusive::new(2,2), &[0]), &[data+1]); - assert_eq!(t.get_or(RangeInclusive::new(3,3), &[0]), &[data+2]); - assert_eq!(t.get_or(RangeInclusive::new(4,4), &[0]), &[0]); - assert_eq!(t.get(RangeInclusive::new(4,4)), None); + t.insert(RangeInclusive::new(1, 1), data); + t.insert(RangeInclusive::new(2, 2), data + 1); + t.insert(RangeInclusive::new(3, 3), data + 2); + assert_eq!(t.get_or(RangeInclusive::new(1, 1), &[0]), &[data]); + assert_eq!(t.get_or(RangeInclusive::new(2, 2), &[0]), &[data + 1]); + assert_eq!(t.get_or(RangeInclusive::new(3, 3), &[0]), &[data + 2]); + assert_eq!(t.get_or(RangeInclusive::new(4, 4), &[0]), &[0]); + assert_eq!(t.get(RangeInclusive::new(4, 4)), None); } #[test] -fn test_contains(){ +fn test_contains() { let data = 1337; let mut t = IntervalTree::::new(); - t.insert(RangeInclusive::new(1,1), data); - t.insert(RangeInclusive::new(2,2), data+1); - t.insert(RangeInclusive::new(3,3), data+2); - assert!(!t.contains(RangeInclusive::new(0,0))); - assert!( t.contains(RangeInclusive::new(1,1))); - assert!( t.contains(RangeInclusive::new(2,2))); - assert!( t.contains(RangeInclusive::new(3,3))); - assert!(!t.contains(RangeInclusive::new(4,4))); + t.insert(RangeInclusive::new(1, 1), data); + t.insert(RangeInclusive::new(2, 2), data + 1); + t.insert(RangeInclusive::new(3, 3), data + 2); + assert!(!t.contains(RangeInclusive::new(0, 0))); + assert!(t.contains(RangeInclusive::new(1, 1))); + assert!(t.contains(RangeInclusive::new(2, 2))); + assert!(t.contains(RangeInclusive::new(3, 3))); + assert!(!t.contains(RangeInclusive::new(4, 4))); } #[test] -fn test_empty(){ +fn test_empty() { let data = 1337; let mut t = IntervalTree::::new(); assert!(t.is_empty()); - t.insert(RangeInclusive::new(1,1), data); - t.insert(RangeInclusive::new(2,2), data+1); - t.insert(RangeInclusive::new(3,3), data+2); + t.insert(RangeInclusive::new(1, 1), data); + t.insert(RangeInclusive::new(2, 2), data + 1); + t.insert(RangeInclusive::new(3, 3), data + 2); assert!(!t.is_empty()); } #[test] -fn test_remove(){ +fn test_remove() { let data = 1337; let mut t = IntervalTree::::new(); - t.insert(RangeInclusive::new(1,1), data); - t.insert(RangeInclusive::new(2,2), data+1); - t.insert(RangeInclusive::new(3,3), data+2); - t.remove(RangeInclusive::new(1,1)); - assert!(!t.contains(RangeInclusive::new(1,1))); - assert!( t.contains(RangeInclusive::new(2,2))); - assert!( t.contains(RangeInclusive::new(3,3))); - t.remove(RangeInclusive::new(2,2)); - assert!(!t.contains(RangeInclusive::new(1,1))); - assert!(!t.contains(RangeInclusive::new(2,2))); - assert!( t.contains(RangeInclusive::new(3,3))); - t.remove(RangeInclusive::new(3,3)); - assert!(!t.contains(RangeInclusive::new(1,1))); - assert!(!t.contains(RangeInclusive::new(2,2))); - assert!(!t.contains(RangeInclusive::new(3,3))); + t.insert(RangeInclusive::new(1, 1), data); + t.insert(RangeInclusive::new(2, 2), data + 1); + t.insert(RangeInclusive::new(3, 3), data + 2); + t.remove(RangeInclusive::new(1, 1)); + assert!(!t.contains(RangeInclusive::new(1, 1))); + assert!(t.contains(RangeInclusive::new(2, 2))); + assert!(t.contains(RangeInclusive::new(3, 3))); + t.remove(RangeInclusive::new(2, 2)); + assert!(!t.contains(RangeInclusive::new(1, 1))); + assert!(!t.contains(RangeInclusive::new(2, 2))); + assert!(t.contains(RangeInclusive::new(3, 3))); + t.remove(RangeInclusive::new(3, 3)); + assert!(!t.contains(RangeInclusive::new(1, 1))); + assert!(!t.contains(RangeInclusive::new(2, 2))); + assert!(!t.contains(RangeInclusive::new(3, 3))); assert!(t.is_empty()); } #[test] -fn test_min(){ +fn test_min() { let mut t = IntervalTree::::new(); - assert!{t.min().is_none()}; - t.insert(RangeInclusive::new(50,50), 1337); - assert_eq!{t.min().expect("get 1 min"),(&RangeInclusive::new(50,50),&[1337][..])}; - t.insert(RangeInclusive::new(49,49),1338); - assert_eq!{t.min().expect("get 2 min"),(&RangeInclusive::new(49,49),&[1338][..])}; - t.insert(RangeInclusive::new(47,47),1339); - assert_eq!{t.min().expect("get 3 min"),(&RangeInclusive::new(47,47),&[1339][..])}; - t.insert(RangeInclusive::new(48,48),1340); - assert_eq!{t.min().expect("get 4 min"),(&RangeInclusive::new(47,47),&[1339][..])}; + assert! {t.min().is_none()}; + t.insert(RangeInclusive::new(50, 50), 1337); + assert_eq! {t.min().expect("get 1 min"),(&RangeInclusive::new(50,50),&[1337][..])}; + t.insert(RangeInclusive::new(49, 49), 1338); + assert_eq! {t.min().expect("get 2 min"),(&RangeInclusive::new(49,49),&[1338][..])}; + t.insert(RangeInclusive::new(47, 47), 1339); + assert_eq! {t.min().expect("get 3 min"),(&RangeInclusive::new(47,47),&[1339][..])}; + t.insert(RangeInclusive::new(48, 48), 1340); + assert_eq! {t.min().expect("get 4 min"),(&RangeInclusive::new(47,47),&[1339][..])}; } #[test] -fn test_iter(){ +fn test_iter() { let mut t = IntervalTree::::new(); - t.insert(RangeInclusive::new(32,32),1337); - t.insert(RangeInclusive::new(34,34),1338); - t.insert(RangeInclusive::new(36,36),1339); - t.insert(RangeInclusive::new(38,38),1340); - for (i,pair) in t.iter().enumerate() { - let (k,v) = pair; - println!("{:?}, {}",k,v); - let key = (i as u64)*2 +32; - assert_eq!(k,RangeInclusive::new(key,key)); - assert_eq!(v,&((i as i32)+1337)); + t.insert(RangeInclusive::new(32, 32), 1337); + t.insert(RangeInclusive::new(34, 34), 1338); + t.insert(RangeInclusive::new(36, 36), 1339); + t.insert(RangeInclusive::new(38, 38), 1340); + for (i, pair) in t.iter().enumerate() { + let (k, v) = pair; + println!("{k:?}, {v}"); + let key = (i as u64) * 2 + 32; + assert_eq!(k, RangeInclusive::new(key, key)); + assert_eq!(v, &((i as i32) + 1337)); } - } #[test] -fn test_range_iter(){ +fn test_range_iter() { let mut t = IntervalTree::::new(); - t.insert(RangeInclusive::new(32,32),1337); - t.insert(RangeInclusive::new(34,34),1338); - t.insert(RangeInclusive::new(36,36),1339); - t.insert(RangeInclusive::new(38,38),1340); - for (i,pair) in t.range(RangeInclusive::new(34, 36)).enumerate() { - let (k,v) = pair; - println!("{:?}, {}",k,v); - let key = (i as u64)*2 +34; - assert_eq!(k,RangeInclusive::new(key,key)); - assert_eq!(v,&((i as i32)+1338)); - assert!(i<2); + t.insert(RangeInclusive::new(32, 32), 1337); + t.insert(RangeInclusive::new(34, 34), 1338); + t.insert(RangeInclusive::new(36, 36), 1339); + t.insert(RangeInclusive::new(38, 38), 1340); + for (i, pair) in t.range(RangeInclusive::new(34, 36)).enumerate() { + let (k, v) = pair; + println!("{k:?}, {v}"); + let key = (i as u64) * 2 + 34; + assert_eq!(k, RangeInclusive::new(key, key)); + assert_eq!(v, &((i as i32) + 1338)); + assert!(i < 2); } - } #[test] -fn test_range_iter_non_pointwise(){ +fn test_range_iter_non_pointwise() { let mut t = IntervalTree::::new(); - t.insert(RangeInclusive::new(3,8),1337); - t.insert(RangeInclusive::new(6,10),1338); - t.insert(RangeInclusive::new(12,36),1339); - t.insert(RangeInclusive::new(32,40),1340); - assert_eq!(t.range(RangeInclusive::new(9,14)).map(|(k,_)| k.start).collect::>(), vec![6,12]) + t.insert(RangeInclusive::new(3, 8), 1337); + t.insert(RangeInclusive::new(6, 10), 1338); + t.insert(RangeInclusive::new(12, 36), 1339); + t.insert(RangeInclusive::new(32, 40), 1340); + assert_eq!( + t.range(RangeInclusive::new(9, 14)) + .map(|(k, _)| k.start) + .collect::>(), + vec![6, 12] + ) } fn random_range() -> RangeInclusive { let offset = rand::random::(); - let len: u64; - if rand::random::() { - len = cmp::min(rand::random::()%500, 0xff_ff_ff_ff_ff_ff_ff_ff - offset) + let len: u64 = if rand::random::() { + cmp::min( + rand::random::() % 500, + 0xff_ff_ff_ff_ff_ff_ff_ff - offset, + ) } else { - len = rand::random::()%(0xff_ff_ff_ff_ff_ff_ff_ff - offset) - } + rand::random::() % (0xff_ff_ff_ff_ff_ff_ff_ff - offset) + }; - return RangeInclusive::new(offset, offset+len) + RangeInclusive::new(offset, offset + len) } #[test] -fn test_range_iter_nontrivial(){ +fn test_range_iter_nontrivial() { let mut set = BTreeSet::>::new(); let mut t = IntervalTree::::new(); for _ in 1..5000 { let decision = rand::random::(); let range = random_range(); - if decision { + if decision { set.insert(range); t.insert(range, 1337); assert!(t.contains(range)); @@ -154,9 +159,16 @@ fn test_range_iter_nontrivial(){ assert!(!t.contains(range)); //assert!(t.test_theban_interval_tree()); }; - let query = random_range(); - let should = set.iter().filter(|r| crate::range::intersect(&query, r)).map(|r| r.clone()).collect::>>(); - let is = t.range(query).map(|(r,_)| r).collect::>>(); - assert_eq!(should, is); - }; + let query = random_range(); + let should = set + .iter() + .filter(|r| crate::range::intersect(&query, r)) + .copied() + .collect::>>(); + let is = t + .range(query) + .map(|(r, _)| r) + .collect::>>(); + assert_eq!(should, is); + } } diff --git a/crates/interval-tree/src/tree.rs b/crates/interval-tree/src/tree.rs index 7e6b399d..c3f7d055 100644 --- a/crates/interval-tree/src/tree.rs +++ b/crates/interval-tree/src/tree.rs @@ -1,7 +1,7 @@ -use std::collections::Bound; +use crate::iterators::{IntoIter, RangePairIter}; +use crate::node::{height, Node}; use crate::range::RangeInclusive; -use crate::node::{Node, height}; -use crate::iterators::{RangePairIter, IntoIter}; +use std::collections::Bound; /// An interval tree. #[derive(Debug, Clone)] @@ -16,60 +16,62 @@ impl Default for IntervalTree { } impl IntervalTree { -/// This function will construct a new empty IntervalTree. -/// # Examples -/// ``` -/// extern crate interval_tree; -/// let mut t=interval_tree::IntervalTree::::new(); -/// ``` + /// This function will construct a new empty IntervalTree. + /// # Examples + /// ``` + /// extern crate interval_tree; + /// let mut t=interval_tree::IntervalTree::::new(); + /// ``` pub fn new() -> Self { IntervalTree { root: None } } -/// This function will return true if the tree is empty, false otherwise. -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// assert!(t.is_empty()); -/// t.insert(interval_tree::range(2,2),25); -/// assert!(!t.is_empty()); -/// -/// ``` - pub fn is_empty(&self) -> bool { self.root.is_none() } + /// This function will return true if the tree is empty, false otherwise. + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// assert!(t.is_empty()); + /// t.insert(interval_tree::range(2,2),25); + /// assert!(!t.is_empty()); + /// + /// ``` + pub fn is_empty(&self) -> bool { + self.root.is_none() + } -/// This function will return the hieght of the tree. An empty tree hash height 0, one with only -/// one elemente has height 1 etc. -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// assert_eq!(t.height(), 0); -/// t.insert(interval_tree::range(2,2),3); -/// assert_eq!(t.height(), 1); -/// -/// ``` + /// This function will return the hieght of the tree. An empty tree hash height 0, one with only + /// one elemente has height 1 etc. + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// assert_eq!(t.height(), 0); + /// t.insert(interval_tree::range(2,2),3); + /// assert_eq!(t.height(), 1); + /// + /// ``` pub fn height(&self) -> usize { height(&self.root) as usize } } impl IntervalTree { -/// This function will insert the key,value pair into the tree, appending to -/// the old data if the key is already part of the tree. -/// -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// t.insert(interval_tree::range(2,2),25); -/// assert_eq!(t.get(interval_tree::range(2,2)), Some(&[25][..])); -/// t.insert(interval_tree::range(2,2),30); -/// assert_eq!(t.get(interval_tree::range(2,2)), Some(&[25, 30][..])); -/// ``` + /// This function will insert the key,value pair into the tree, appending to + /// the old data if the key is already part of the tree. + /// + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// t.insert(interval_tree::range(2,2),25); + /// assert_eq!(t.get(interval_tree::range(2,2)), Some(&[25][..])); + /// t.insert(interval_tree::range(2,2),30); + /// assert_eq!(t.get(interval_tree::range(2,2)), Some(&[25, 30][..])); + /// ``` pub fn insert(&mut self, key: RangeInclusive, data: V) { self.root = Some(match self.root.take() { Some(box_to_node) => box_to_node.insert(key, data), @@ -77,117 +79,111 @@ impl IntervalTree { }); } -/// This function will remove the key,value pair from the tree, doing nothing if the key is not -/// part of the tree. -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// t.insert(interval_tree::range(2,2),25); -/// t.remove(interval_tree::range(2,2)); -/// assert!(t.is_empty()); -/// // deleting nonexistant keys doesn't do anything -/// t.remove(interval_tree::range(3,3)); -/// assert!(t.is_empty()); -/// ``` + /// This function will remove the key,value pair from the tree, doing nothing if the key is not + /// part of the tree. + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// t.insert(interval_tree::range(2,2),25); + /// t.remove(interval_tree::range(2,2)); + /// assert!(t.is_empty()); + /// // deleting nonexistant keys doesn't do anything + /// t.remove(interval_tree::range(3,3)); + /// assert!(t.is_empty()); + /// ``` pub fn remove(&mut self, key: RangeInclusive) { - match self.root.take() { - Some(box_to_node) => self.root = box_to_node.delete(key), - None => return + if let Some(box_to_node) = self.root.take() { + self.root = box_to_node.delete(key); } } -/// This function will return the Some(data) stored under the given key or None if the key is not -/// known. -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// t.insert(interval_tree::range(2,2),25); -/// assert_eq!(t.get(interval_tree::range(2,2)), Some(&[25][..])); -/// assert_eq!(t.get(interval_tree::range(3,3)), None); -/// -/// ``` + /// This function will return the Some(data) stored under the given key or None if the key is not + /// known. + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// t.insert(interval_tree::range(2,2),25); + /// assert_eq!(t.get(interval_tree::range(2,2)), Some(&[25][..])); + /// assert_eq!(t.get(interval_tree::range(3,3)), None); + /// + /// ``` pub fn get(&self, key: RangeInclusive) -> Option<&[V]> { match self.root { Some(ref box_to_node) => box_to_node.search(&key), - None => None + None => None, } } -/// This function will return the data stored under the given key or the default if the key is not -/// known. -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// t.insert(interval_tree::range(2,2),25); -/// assert_eq!(t.get_or(interval_tree::range(2,2),&[2000]), &[25]); -/// assert_eq!(t.get_or(interval_tree::range(3,3),&[2000]), &[2000]); -/// -/// ``` + /// This function will return the data stored under the given key or the default if the key is not + /// known. + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// t.insert(interval_tree::range(2,2),25); + /// assert_eq!(t.get_or(interval_tree::range(2,2),&[2000]), &[25]); + /// assert_eq!(t.get_or(interval_tree::range(3,3),&[2000]), &[2000]); + /// + /// ``` pub fn get_or<'a>(&'a self, key: RangeInclusive, default: &'a [V]) -> &'a [V] { self.get(key).unwrap_or(default) } -/// This function will return true if the tree contains the given key, false otherwise -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// t.insert(interval_tree::range(2,2),25); -/// assert!(!t.contains(interval_tree::range(3,3))); -/// assert!(t.contains(interval_tree::range(2,2))); -/// -/// ``` + /// This function will return true if the tree contains the given key, false otherwise + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// t.insert(interval_tree::range(2,2),25); + /// assert!(!t.contains(interval_tree::range(3,3))); + /// assert!(t.contains(interval_tree::range(2,2))); + /// + /// ``` pub fn contains(&self, key: RangeInclusive) -> bool { self.get(key).is_some() } -/// This function will return the key/value pair with the smallest key in the tree, or None if the -/// tree is empty. -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// t.insert(interval_tree::range(2,2),25); -/// t.insert(interval_tree::range(3,3),50); -/// assert_eq!(t.min().unwrap().0, &interval_tree::range(2,2)); -/// assert_eq!(t.min().unwrap().1, &[25]); -/// -/// ``` - pub fn min<'a>(&'a self) -> Option<(&'a RangeInclusive, &'a [V])> { + /// This function will return the key/value pair with the smallest key in the tree, or None if the + /// tree is empty. + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// t.insert(interval_tree::range(2,2),25); + /// t.insert(interval_tree::range(3,3),50); + /// assert_eq!(t.min().unwrap().0, &interval_tree::range(2,2)); + /// assert_eq!(t.min().unwrap().1, &[25]); + /// + /// ``` + pub fn min(&self) -> Option<(&'_ RangeInclusive, &'_ [V])> { self.root.as_ref().map(|n| n.min_pair()) } -/// This function will return the key/value pair with the biggest key in the tree, or None if the -/// tree is empty. -/// # Examples -/// ``` -/// extern crate interval_tree; -/// -/// let mut t=interval_tree::IntervalTree::::new(); -/// t.insert(interval_tree::range(2,2),25); -/// t.insert(interval_tree::range(3,3),50); -/// assert_eq!(t.max().unwrap().0, &interval_tree::range(3,3)); -/// assert_eq!(t.max().unwrap().1, &[50]); -/// -/// ``` - pub fn max<'a>(&'a self) -> Option<(&'a RangeInclusive, &'a [V])> { + /// This function will return the key/value pair with the biggest key in the tree, or None if the + /// tree is empty. + /// # Examples + /// ``` + /// extern crate interval_tree; + /// + /// let mut t=interval_tree::IntervalTree::::new(); + /// t.insert(interval_tree::range(2,2),25); + /// t.insert(interval_tree::range(3,3),50); + /// assert_eq!(t.max().unwrap().0, &interval_tree::range(3,3)); + /// assert_eq!(t.max().unwrap().1, &[50]); + /// + /// ``` + pub fn max(&self) -> Option<(&'_ RangeInclusive, &'_ [V])> { self.root.as_ref().map(|n| n.max_pair()) } -/// Return an iterator for all (key,value) pairs in the tree, consuming the tree in the process. - pub fn into_iter(self) -> IntoIter { - IntoIter::new(self) - } - -/// Merge all (key, value) pairs from another tree into this one, consuming it. + /// Merge all (key, value) pairs from another tree into this one, consuming it. pub fn merge(&mut self, other: IntervalTree) { for (k, v) in other.into_iter() { self.insert(k, v); @@ -195,32 +191,47 @@ impl IntervalTree { } } +impl IntoIterator for IntervalTree { + type Item = as Iterator>::Item; + + type IntoIter = IntoIter; + + /// Return an iterator for all (key,value) pairs in the tree, consuming the tree in the process. + fn into_iter(self) -> Self::IntoIter { + Self::IntoIter::new(self) + } +} + impl IntervalTree { -/// This function will return a read only iterator for all (key,value) pairs between the two -/// bounds. -/// # Examples -/// ``` -/// //[...] -/// # let mut t=interval_tree::IntervalTree::::new(); -/// for (key,val) in t.range(interval_tree::range(9, 100)) { -/// println!("{:?} -> {}",key,val) -/// } -/// -/// ``` - pub fn range(&self, range: RangeInclusive) -> RangePairIter { - RangePairIter::new(self, Bound::Included(range.start), Bound::Included(range.end)) + /// This function will return a read only iterator for all (key,value) pairs between the two + /// bounds. + /// # Examples + /// ``` + /// //[...] + /// # let mut t=interval_tree::IntervalTree::::new(); + /// for (key,val) in t.range(interval_tree::range(9, 100)) { + /// println!("{:?} -> {}",key,val) + /// } + /// + /// ``` + pub fn range(&self, range: RangeInclusive) -> RangePairIter<'_, K, V> { + RangePairIter::new( + self, + Bound::Included(range.start), + Bound::Included(range.end), + ) } -/// This function will return a read only iterator for all (key,value) pairs in the tree. -/// # Examples -/// ``` -/// # let mut t=interval_tree::IntervalTree::::new(); -/// for (key,val) in t.iter() { -/// println!("{:?} -> {}",key,val) -/// } -/// -/// ``` - pub fn iter(&self) -> RangePairIter { + /// This function will return a read only iterator for all (key,value) pairs in the tree. + /// # Examples + /// ``` + /// # let mut t=interval_tree::IntervalTree::::new(); + /// for (key,val) in t.iter() { + /// println!("{:?} -> {}",key,val) + /// } + /// + /// ``` + pub fn iter(&self) -> RangePairIter<'_, K, V> { RangePairIter::new(self, Bound::Unbounded, Bound::Unbounded) } } @@ -228,25 +239,24 @@ impl IntervalTree { #[cfg(test)] mod tests { extern crate rand; - use crate::range::RangeInclusive; use crate::node::tests::is_interval_tree; + use crate::range::RangeInclusive; type IntervalTree = super::IntervalTree; fn random_range() -> RangeInclusive { - let offset = rand::random::()%50; - let len: u64; - len = rand::random::()%50; - crate::range(offset, offset+len) + let offset = rand::random::() % 50; + let len: u64 = rand::random::() % 50; + crate::range(offset, offset + len) } #[test] - fn test_fuzz(){ + fn test_fuzz() { let mut t = IntervalTree::::new(); for _ in 1..5000 { let decision = rand::random::(); let range = random_range(); - if decision { + if decision { t.insert(range, 1337); assert!(t.contains(range)); assert!(is_interval_tree(&t.root)); @@ -255,7 +265,6 @@ mod tests { assert!(!t.contains(range)); assert!(is_interval_tree(&t.root)); }; - }; - return; + } } } diff --git a/crates/spaceman-dmm/Cargo.toml b/crates/spaceman-dmm/Cargo.toml index 6763ab79..f8e44e50 100644 --- a/crates/spaceman-dmm/Cargo.toml +++ b/crates/spaceman-dmm/Cargo.toml @@ -2,7 +2,7 @@ name = "spaceman-dmm" version = "0.1.0" authors = ["Tad Hardesty "] -edition = "2018" +edition = "2021" [[bin]] name = "editor" @@ -11,28 +11,28 @@ path = "src/main.rs" [dependencies] dreammaker = { path = "../dreammaker" } dmm-tools = { path = "../tools", features = ["gfx_core"] } -glutin = "0.21.1" -gfx_gl = "0.6.0" +glutin = "0.28.0" +gfx_gl = "0.6.1" gfx = "0.18" gfx_core = "0.9.2" gfx_window_glutin = "0.31.0" gfx_device_gl = "0.16.2" -imgui = "0.2.1" -imgui-gfx-renderer = "0.2.0" +imgui = "0.8.2" +imgui-gfx-renderer = "0.8.2" lodepng = "3.0.0" -ndarray = "0.13.0" -divrem = "0.1.0" -serde = "1.0.76" -serde_derive = "1.0.76" -toml = "0.5.5" -petgraph = { version = "0.5.0", default-features = false } -weak-table = "0.2.3" -slice-of-array = "0.2.0" +ndarray = "0.15.4" +divrem = "1.0.0" +serde = "1.0.136" +serde_derive = "1.0.136" +toml = "0.5.9" +petgraph = { version = "0.6.0", default-features = false } +weak-table = "0.3.2" +slice-of-array = "0.3.2" [dependencies.nfd] git = "https://github.com/SpaceManiac/nfd-rs" branch = "zenity" [build-dependencies] -chrono = "0.4.0" -git2 = { version = "0.13", default-features = false } +chrono = "0.4.19" +git2 = { version = "0.20.2", default-features = false } diff --git a/crates/spaceman-dmm/build.rs b/crates/spaceman-dmm/build.rs index fa2ca3cf..6c6dfba1 100644 --- a/crates/spaceman-dmm/build.rs +++ b/crates/spaceman-dmm/build.rs @@ -15,7 +15,7 @@ fn main() { if let Ok(commit) = read_commit() { writeln!(f, "commit: {}", commit).unwrap(); } - writeln!(f, "build date: {}", chrono::Utc::today()).unwrap(); + writeln!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap(); // windres icon if cfg!(windows) { diff --git a/crates/spaceman-dmm/src/main.rs b/crates/spaceman-dmm/src/main.rs index 47ed799e..09de2d4c 100644 --- a/crates/spaceman-dmm/src/main.rs +++ b/crates/spaceman-dmm/src/main.rs @@ -590,7 +590,7 @@ impl EditorScene { ui.menu(im_str!("Help"), true, || { ui.menu(im_str!("About SpacemanDMM"), true, || { ui.text(&im_str!( - "{} {} Copyright (C) 2017-2021 Tad Hardesty", + "{} {} Copyright (C) 2017-2025 Tad Hardesty", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), )); diff --git a/dockerfile b/dockerfile deleted file mode 100644 index 56597d06..00000000 --- a/dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM rust:1.54 - -WORKDIR /usr/src/myapp -COPY . . - -RUN cargo build -p cli --release -RUN ln -s /usr/src/myapp/target/release/dmm-tools /usr/src/myapp/dmm-tools -ENTRYPOINT ["./dmm-tools"] -CMD ["help"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..8c795ae5 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +match_block_trailing_comma = true diff --git a/scripts/build-langserver-release.sh b/scripts/build-langserver-release.sh index bf610ed5..a2eb6027 100755 --- a/scripts/build-langserver-release.sh +++ b/scripts/build-langserver-release.sh @@ -4,11 +4,6 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" cd .. -# ----------------------------------------------------------------------------- -# Prepare dependency DLLs -eval "$(scripts/download-extools.sh)" -eval "$(scripts/download-auxtools.sh)" - # ----------------------------------------------------------------------------- # Cargo build touch crates/dm-langserver/build.rs diff --git a/scripts/build-suite-release.sh b/scripts/build-suite-release.sh index b3629b7e..84e128d4 100755 --- a/scripts/build-suite-release.sh +++ b/scripts/build-suite-release.sh @@ -9,11 +9,6 @@ cd .. relname=$(git describe --tags --exact) echo "Using tag name: $relname" -# ----------------------------------------------------------------------------- -# Prepare dependency DLLs -eval "$(scripts/download-extools.sh)" -eval "$(scripts/download-auxtools.sh)" - # ----------------------------------------------------------------------------- # Cargo build touch crates/*/build.rs diff --git a/scripts/download-auxtools.sh b/scripts/download-auxtools.sh deleted file mode 100755 index afb16a98..00000000 --- a/scripts/download-auxtools.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Settings -DEBUG_SERVER_TAG=v2.2.2 -DEBUG_SERVER_DLL_URL=https://github.com/willox/auxtools/releases/download/v2.2.2/debug_server.dll -DEBUG_SERVER_DLL_SHA256=847c3bfbd35f2eb57f43d0ba08df323c0f0beb1f7d518e7e54791a1aff13a56f - -# ----------------------------------------------------------------------------- -cd "$(dirname "${BASH_SOURCE[0]}")" -mkdir -p "../target/deps" -cd "../target/deps" - -AUXTOOLS_BUNDLE_DLL="$PWD/debug_server.dll" -echo "export AUXTOOLS_BUNDLE_DLL=$AUXTOOLS_BUNDLE_DLL" -echo "export AUXTOOLS_COMMIT_HASH=$DEBUG_SERVER_TAG" - -if ! test -f "$AUXTOOLS_BUNDLE_DLL" || ! sha256sum -c <<<"$DEBUG_SERVER_DLL_SHA256 $AUXTOOLS_BUNDLE_DLL" >/dev/null 2>/dev/null; then - wget -q -O "$AUXTOOLS_BUNDLE_DLL" "$DEBUG_SERVER_DLL_URL" >&2 - sha256sum -c <<<"$DEBUG_SERVER_DLL_SHA256 $AUXTOOLS_BUNDLE_DLL" >&2 -fi diff --git a/scripts/download-extools.sh b/scripts/download-extools.sh deleted file mode 100755 index 81d3f432..00000000 --- a/scripts/download-extools.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Settings -EXTOOLS_TAG=v0.0.7 -EXTOOLS_DLL_URL=https://github.com/tgstation/tgstation/raw/34f0cc6394a064b87cbd1d6cb225f1d3df444ba7/byond-extools.dll -EXTOOLS_DLL_SHA256=073dd08790a13580bae71758e9217917700dd85ce8d35cb030cef0cf5920fca8 - -# ----------------------------------------------------------------------------- -cd "$(dirname "${BASH_SOURCE[0]}")" -mkdir -p "../target/deps" -cd "../target/deps" - -EXTOOLS_BUNDLE_DLL="$PWD/extools.dll" -echo "export EXTOOLS_BUNDLE_DLL=$EXTOOLS_BUNDLE_DLL" -echo "export EXTOOLS_COMMIT_HASH=$EXTOOLS_TAG" - -if ! test -f "$EXTOOLS_BUNDLE_DLL" || ! sha256sum -c <<<"$EXTOOLS_DLL_SHA256 $EXTOOLS_BUNDLE_DLL" >/dev/null 2>/dev/null; then - wget -q -O "$EXTOOLS_BUNDLE_DLL" "$EXTOOLS_DLL_URL" >&2 - sha256sum -c <<<"$EXTOOLS_DLL_SHA256 $EXTOOLS_BUNDLE_DLL" >&2 -fi diff --git a/scripts/update-auxtools.sh b/scripts/update-auxtools.sh index b81c87cd..feb967b4 100755 --- a/scripts/update-auxtools.sh +++ b/scripts/update-auxtools.sh @@ -12,12 +12,11 @@ DEBUG_SERVER_DLL_URL=https://github.com/willox/auxtools/releases/download/$DEBUG DEBUG_SERVER_DLL_SHA256=$(curl -L -s "$DEBUG_SERVER_DLL_URL" | sha256sum | cut -d' ' -f1) sed \ - -e "/^DEBUG_SERVER_TAG=/c DEBUG_SERVER_TAG=$DEBUG_SERVER_TAG" \ - -e "/^DEBUG_SERVER_DLL_URL=/c DEBUG_SERVER_DLL_URL=$DEBUG_SERVER_DLL_URL" \ - -e "/^DEBUG_SERVER_DLL_SHA256=/c DEBUG_SERVER_DLL_SHA256=$DEBUG_SERVER_DLL_SHA256" \ + -e "/DEBUG_SERVER_TAG$/c\\ \"$DEBUG_SERVER_TAG\", // DEBUG_SERVER_TAG" \ + -e "/DEBUG_SERVER_DLL_URL$/c\\ \"$DEBUG_SERVER_DLL_URL\", // DEBUG_SERVER_DLL_URL" \ + -e "/DEBUG_SERVER_DLL_SHA256$/c\\ \"$DEBUG_SERVER_DLL_SHA256\", // DEBUG_SERVER_DLL_SHA256" \ --in-place \ - download-auxtools.sh + ../crates/dm-langserver/build.rs # Prepare the commit -git add download-auxtools.sh -git commit -m "Update to auxtools debug server $DEBUG_SERVER_TAG" +git commit -m "Update to auxtools debug server $DEBUG_SERVER_TAG" -- ../crates/dm-langserver/build.rs