numbat-1.11.0/.cargo_vcs_info.json0000644000000001440000000000100124010ustar { "git": { "sha1": "46f6b0e2943e3b2535eade9f7f277e88e7b10d08" }, "path_in_vcs": "numbat" }numbat-1.11.0/Cargo.lock0000644000001157520000000000100103700ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "approx" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" dependencies = [ "num-traits", ] [[package]] name = "arrayvec" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "attohttpc" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "184f5e6cce583a9db6b6f8d772a42cfce5b78e7c3ef26118cec3ce4c8c14969b" dependencies = [ "http", "log", "rustls", "url", "webpki-roots", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets 0.52.0", ] [[package]] name = "chrono-tz" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7" dependencies = [ "chrono", "chrono-tz-build", "phf", ] [[package]] name = "chrono-tz-build" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" dependencies = [ "parse-zoneinfo", "phf", "phf_codegen", ] [[package]] name = "ciborium" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" [[package]] name = "ciborium-ll" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "codespan-reporting" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", "unicode-width", ] [[package]] name = "console" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "windows-sys 0.52.0", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools 0.10.5", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools 0.10.5", ] [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "half" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "html-escape" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" dependencies = [ "utf8-width", ] [[package]] name = "http" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "iana-time-zone" version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "insta" version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" dependencies = [ "console", "lazy_static", "linked-hash-map", "similar", "yaml-rust", ] [[package]] name = "is-terminal" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi", "rustix", "windows-sys 0.52.0", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libredox" version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.4.2", "libc", "redox_syscall", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "num-bigint" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-format" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" dependencies = [ "arrayvec", "itoa", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-rational" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-bigint", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "numbat" version = "1.11.0" dependencies = [ "approx", "chrono", "chrono-tz", "codespan-reporting", "criterion", "glob", "heck", "html-escape", "iana-time-zone", "insta", "itertools 0.12.0", "libc", "num-format", "num-integer", "num-rational", "num-traits", "numbat-exchange-rates", "once_cell", "pretty_dtoa", "rust-embed", "strsim", "termcolor", "thiserror", "unicode-ident", "unicode-width", "walkdir", ] [[package]] name = "numbat-exchange-rates" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd1e3c3e4f9f22d0d7cdcb413f01194f6506a302a9029d95deedcd1c25df7718" dependencies = [ "attohttpc", "quick-xml", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[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.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_shared" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "plotters" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] [[package]] name = "pretty_dtoa" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a239bcdfda2c685fda1add3b4695c06225f50075e3cfb5b954e91545587edff2" dependencies = [ "ryu_floating_decimal", ] [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rayon" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", "libredox", "thiserror", ] [[package]] name = "regex" version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ring" version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom", "libc", "spin", "untrusted", "windows-sys 0.48.0", ] [[package]] name = "rust-embed" version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f" dependencies = [ "rust-embed-impl", "rust-embed-utils", "walkdir", ] [[package]] name = "rust-embed-impl" version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", "shellexpand", "syn", "walkdir", ] [[package]] name = "rust-embed-utils" version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665" dependencies = [ "sha2", "walkdir", ] [[package]] name = "rustix" version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" dependencies = [ "log", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e9d979b3ce68192e42760c7810125eb6cf2ea10efae545a156063e61f314e2a" [[package]] name = "rustls-webpki" version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4ca26037c909dedb327b48c3327d0ba91d3dd3c4e05dad328f210ffb68e95b" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "ryu" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "ryu_floating_decimal" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "700de91d5fd6091442d00fdd9ee790af6d4f0f480562b0f5a1e8f59e90aafe73" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "serde" version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shellexpand" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" dependencies = [ "dirs", ] [[package]] name = "similar" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf8-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "web-sys" version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-roots" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de2cfda980f21be5a7ed2eadb3e6fe074d56022bea2cdeb1a62eb220fc04188" dependencies = [ "rustls-pki-types", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.0", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[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.0", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm 0.52.0", "windows_aarch64_msvc 0.52.0", "windows_i686_gnu 0.52.0", "windows_i686_msvc 0.52.0", "windows_x86_64_gnu 0.52.0", "windows_x86_64_gnullvm 0.52.0", "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] [[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" numbat-1.11.0/Cargo.toml0000644000000050070000000000100104020ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70" name = "numbat" version = "1.11.0" authors = ["David Peter "] description = "A statically typed programming language for scientific computations with first class support for physical dimensions and units." homepage = "https://numbat.dev/" readme = "README.md" keywords = [ "language", "compiler", "physics", "units", "calculation", ] categories = [ "science", "mathematics", "compilers", ] license = "MIT OR Apache-2.0" repository = "https://github.com/sharkdp/numbat" [[bench]] name = "prelude" harness = false [dependencies.chrono] version = "0.4.31" [dependencies.chrono-tz] version = "0.8.5" [dependencies.codespan-reporting] version = "0.11" [dependencies.heck] version = "0.4.1" features = ["unicode"] [dependencies.html-escape] version = "0.2.13" optional = true [dependencies.iana-time-zone] version = "0.1" [dependencies.itertools] version = "0.12" [dependencies.libc] version = "0.2.152" [dependencies.num-format] version = "0.4.4" [dependencies.num-integer] version = "0.1.45" [dependencies.num-rational] version = "0.4" [dependencies.num-traits] version = "0.2" [dependencies.numbat-exchange-rates] version = "0.5.0" [dependencies.pretty_dtoa] version = "0.3" [dependencies.rust-embed] version = "8.2.0" features = [ "interpolate-folder-path", "debug-embed", ] [dependencies.strsim] version = "0.11.0" [dependencies.termcolor] version = "1.4.1" optional = true [dependencies.thiserror] version = "1" [dependencies.unicode-ident] version = "1.0.12" [dependencies.unicode-width] version = "0.1.11" [dependencies.walkdir] version = "2" [dev-dependencies.approx] version = "0.5" [dev-dependencies.criterion] version = "0.5" features = ["html_reports"] [dev-dependencies.glob] version = "0.3" [dev-dependencies.insta] version = "1.34.0" [dev-dependencies.once_cell] version = "1.19.0" [features] default = ["fetch-exchangerates"] fetch-exchangerates = ["numbat-exchange-rates/fetch-exchangerates"] html-formatter = [ "termcolor", "html-escape", ] numbat-1.11.0/Cargo.toml.orig000064400000000000000000000030241046102023000140600ustar 00000000000000[package] name = "numbat" description = "A statically typed programming language for scientific computations with first class support for physical dimensions and units." authors = ["David Peter "] categories = ["science", "mathematics", "compilers"] keywords = ["language", "compiler", "physics", "units", "calculation"] homepage = "https://numbat.dev/" repository = "https://github.com/sharkdp/numbat" version = "1.11.0" edition = "2021" license = "MIT OR Apache-2.0" readme = "README.md" rust-version = "1.70" [dependencies] thiserror = "1" itertools = "0.12" num-rational = "0.4" num-integer = "0.1.45" num-traits = "0.2" codespan-reporting = "0.11" strsim = "0.11.0" pretty_dtoa = "0.3" numbat-exchange-rates = { version = "0.5.0", path = "../numbat-exchange-rates" } heck = { version = "0.4.1", features = ["unicode"] } unicode-ident = "1.0.12" unicode-width = "0.1.11" libc = "0.2.152" rust-embed = { version = "8.2.0", features = ["interpolate-folder-path", "debug-embed"] } num-format = "0.4.4" walkdir = "2" chrono = "0.4.31" chrono-tz = "0.8.5" iana-time-zone = "0.1" termcolor = { version = "1.4.1", optional = true } html-escape = { version = "0.2.13", optional = true } [features] default = ["fetch-exchangerates"] fetch-exchangerates = ["numbat-exchange-rates/fetch-exchangerates"] html-formatter = ["termcolor", "html-escape"] [dev-dependencies] approx = "0.5" glob = "0.3" insta = "1.34.0" once_cell = "1.19.0" criterion = { version = "0.5", features = ["html_reports"] } [[bench]] name = "prelude" harness = false numbat-1.11.0/README.md000064400000000000000000000004531046102023000124530ustar 00000000000000# Numbat *Numbat* is a statically typed programming language for scientific computations with first class support for physical units. This crate contains the compiler for the Numbat language, as a library. The command line tool is available as [`numbat-cli`](https://crates.io/crates/numbat-cli). numbat-1.11.0/benches/prelude.rs000064400000000000000000000010671046102023000146130ustar 00000000000000use criterion::{criterion_group, criterion_main, Criterion}; use numbat::module_importer::BuiltinModuleImporter; use numbat::resolver::CodeSource; use numbat::Context; fn import_prelude(c: &mut Criterion) { let importer = BuiltinModuleImporter::default(); let context = Context::new(importer); c.bench_function("Import prelude", |b| { b.iter_with_setup( || context.clone(), |mut ctx| ctx.interpret("use prelude", CodeSource::Text), ) }); } criterion_group!(benches, import_prelude); criterion_main!(benches); numbat-1.11.0/examples/inspect.rs000064400000000000000000000033341046102023000150260ustar 00000000000000use itertools::Itertools; use numbat::{module_importer::FileSystemImporter, resolver::CodeSource, Context}; use std::path::Path; fn main() { let module_path = Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("modules"); let mut importer = FileSystemImporter::default(); importer.add_path(module_path); let mut ctx = Context::new(importer); let _result = ctx.interpret("use all", CodeSource::Internal).unwrap(); println!(" # List of supported units See also: [Unit notation](./unit-notation.md). All SI-accepted units support [metric prefixes](https://en.wikipedia.org/wiki/Metric_prefix) (`mm`, `cm`, `km`, ... or `millimeter`, `centimeter`, `kilometer`, ...) and — where sensible — units allow for [binary prefixes](https://en.wikipedia.org/wiki/Binary_prefix) (`MiB`, `GiB`, ... or `mebibyte`, `gibibyte`, ...). "); println!("| Dimension | Unit name | Identifier(s) |"); println!("| --- | --- | --- |"); for (ref unit_name, (_base_representation, unit_metadata)) in ctx .unit_representations() .sorted_by_key(|(u, (_, m))| (m.readable_type.to_string(), u.to_lowercase())) { let mut names = unit_metadata.aliases; names.sort_by_key(|(n, _)| n.to_lowercase()); let names = names.iter().map(|(n, _)| n).join("`, `"); let url = unit_metadata.url; let name = unit_metadata.name.unwrap_or(unit_name.clone()); let name_with_url = if let Some(url) = url { format!("[{name}]({url})") } else { name.clone() }; let readable_type = unit_metadata.readable_type; println!("| `{readable_type}` | {name_with_url} | `{names}` |"); } } numbat-1.11.0/examples/unit_graph.rs000064400000000000000000000027271046102023000155260ustar 00000000000000// You can run this program to generate a graph view of all Numbat units // and their connections to base units: // // cargo run --example=unit_graph | dot -Tsvg -o units.svg // use itertools::Itertools; use numbat::{ module_importer::FileSystemImporter, resolver::CodeSource, BaseRepresentationFactor, Context, }; fn main() { let mut importer = FileSystemImporter::default(); importer.add_path("numbat/modules"); let mut ctx = Context::new(importer); let _ = ctx.interpret("use all", CodeSource::Internal).unwrap(); println!("digraph G {{"); println!(" layout=fdp;"); println!(" splines=true;"); println!(" rankdir=LR;"); println!(" overlap=false;"); for unit_name in ctx.base_units().sorted() { println!(" {unit_name} [color=\"#eaea5e\",style=filled,shape=doublecircle]"); } for (ref unit_name, base_representation) in ctx.unit_representations().map(|(u, b)| { ( u, b.0.iter() .map(|BaseRepresentationFactor(name, exp)| (name.clone(), exp.to_integer())) .collect::>(), ) }) // TODO: check if to_integer can fail here.sorted_by_key(|(_, b)| b.clone()) { let is_base = base_representation == vec![(unit_name.into(), 1i128)]; if !is_base { for (base_factor, _) in base_representation { println!(" {} -> {}", unit_name, base_factor); } } } println!("}}"); } numbat-1.11.0/modules/all.nbt000064400000000000000000000001071046102023000141150ustar 00000000000000use prelude use units::currencies use units::stoney use units::hartree numbat-1.11.0/modules/core/dimensions.nbt000064400000000000000000000072701046102023000164550ustar 00000000000000### Physical dimensions dimension Angle = 1 # SI: plane angle dimension SolidAngle = Angle^2 dimension Length dimension Area = Length^2 dimension Volume = Length^3 dimension Wavenumber = 1 / Length dimension Time dimension Frequency = 1 / Time dimension Velocity = Length / Time dimension Acceleration = Length / Time^2 dimension Jerk = Length / Time^3 dimension FlowRate = Volume / Time dimension Mass dimension Momentum = Mass × Velocity dimension Force = Mass × Acceleration = Momentum / Time dimension Energy = Momentum^2 / Mass = Mass × Velocity^2 = Force × Length # also: work, amount of heat dimension Power = Energy / Time = Force × Velocity dimension Pressure = Force / Area = Energy / Volume # also: stress dimension Action = Energy × Time dimension MassDensity = Mass / Length^3 dimension MomentOfInertia = Mass × Length^2 / Angle^2 dimension AngularMomentum = MomentOfInertia × Angle / Time = Mass × Length^2 / Time / Angle dimension Torque = Length × Force / Angle # also: moment of force dimension EnergyDensity = Energy / Volume dimension MassFlow = Mass / Time dimension Current dimension ElectricCharge = Current × Time dimension Voltage = Energy / ElectricCharge = Power / Current # ISQ: electric tension, SI: electric potential difference dimension Capacitance = ElectricCharge / Voltage dimension ElectricResistance = Voltage / Current dimension Resistivity = ElectricResistance × Length dimension ElectricConductance = 1 / ElectricResistance dimension Conductivity = ElectricConductance / Length dimension MagneticFluxDensity = Force / (ElectricCharge × Velocity) dimension MagneticFlux = MagneticFluxDensity × Area = Voltage × Time dimension MagneticFieldStrength = Current / Length dimension Inductance = MagneticFlux / Current dimension ElectricChargeDensity = ElectricCharge / Volume dimension CurrentDensity = Current / Area dimension ElectricDipoleMoment = ElectricCharge × Length dimension ElectricQuadrupoleMoment = ElectricCharge × Length^2 dimension MagneticDipoleMoment = Current × Area = Torque / MagneticFluxDensity dimension ElectricFieldStrength = Voltage / Length dimension ElectricDisplacementFieldStrength = ElectricCharge / Area dimension ElectricPermittivity = Time^4 × Current^2 / Mass / Length^3 × Angle = ElectricDisplacementFieldStrength / ElectricFieldStrength × Angle dimension MagneticPermeability = Length × Mass / Time^2 / Current^2 / Angle = MagneticFluxDensity / MagneticFieldStrength / Angle dimension Polarizability = ElectricDipoleMoment / ElectricFieldStrength = Current^2 × Time^4 / Mass dimension ElectricMobility = Velocity / ElectricFieldStrength dimension Temperature dimension Entropy = Energy / Temperature dimension HeatCapacity = Energy / Temperature dimension SpecificHeatCapacity = HeatCapacity / Mass dimension ThermalConductivity = Power / (Length × Temperature) dimension ThermalTransmittance = Power / (Length^2 × Temperature) dimension AmountOfSubstance dimension MolarMass = Mass / AmountOfSubstance dimension MolarVolume = Volume / AmountOfSubstance dimension CatalyticActivity = AmountOfSubstance / Time dimension Molarity = AmountOfSubstance / Volume dimension Molality = AmountOfSubstance / Mass dimension ChemicalPotential = Energy / AmountOfSubstance dimension MolarHeatCapacity = HeatCapacity / AmountOfSubstance dimension LuminousIntensity dimension LuminousFlux = LuminousIntensity × Angle^2 dimension Illuminance = LuminousFlux / Area dimension Irradiance = Power / Area dimension Activity = 1 / Time dimension AbsorbedDose = Energy / Mass dimension EquivalentDose = Energy / Mass # also: dose equivalent dimension SpecificActivity = Activity / Mass dimension DynamicViscosity = Pressure × Time dimension KinematicViscosity = Length^2 / Time numbat-1.11.0/modules/core/error.nbt000064400000000000000000000002441046102023000154300ustar 00000000000000# TODO: Ideally, this function should have a '-> !' return type such that # it can be used everywhere. Not just instead of a Scalar. fn error(message: String) -> 1 numbat-1.11.0/modules/core/functions.nbt000064400000000000000000000001641046102023000163100ustar 00000000000000fn abs(x: T) -> T fn round(x: T) -> T fn floor(x: T) -> T fn ceil(x: T) -> T fn mod(a: T, b: T) -> T numbat-1.11.0/modules/core/quantities.nbt000064400000000000000000000001331046102023000164620ustar 00000000000000use core::scalar fn unit_of(x: T) -> T fn value_of(x: T) -> Scalar = x / unit_of(x) numbat-1.11.0/modules/core/scalar.nbt000064400000000000000000000000251046102023000155410ustar 00000000000000dimension Scalar = 1 numbat-1.11.0/modules/core/strings.nbt000064400000000000000000000053411046102023000157730ustar 00000000000000use core::scalar use core::functions fn str_length(s: String) -> Scalar fn str_slice(s: String, start: Scalar, end: Scalar) -> String fn chr(n: Scalar) -> String fn lowercase(s: String) -> String fn uppercase(s: String) -> String fn str_append(a: String, b: String) -> String = "{a}{b}" fn str_contains(haystack: String, needle: String) -> Bool = if str_length(haystack) == 0 then false else if str_slice(haystack, 0, str_length(needle)) == needle then true else str_contains(str_slice(haystack, 1, str_length(haystack)), needle) fn str_replace(s: String, pattern: String, replacement: String) -> String = if pattern == "" then s else if str_contains(s, pattern) then if str_slice(s, 0, str_length(pattern)) == pattern then str_append(replacement, str_replace(str_slice(s, str_length(pattern), str_length(s)), pattern, replacement)) else str_append(str_slice(s, 0, 1), str_replace(str_slice(s, 1, str_length(s)), pattern, replacement)) else s fn str_repeat(a: String, n: Scalar) -> String = if n > 0 then str_append(a, str_repeat(a, n - 1)) else "" fn _bin_digit(x: Scalar) -> String = chr(48 + mod(x, 2)) fn _oct_digit(x: Scalar) -> String = chr(48 + mod(x, 8)) fn _hex_digit(x: Scalar) -> String = if mod(x, 16) < 10 then chr(48 + mod(x, 16)) else chr(97 + mod(x, 16) - 10) fn _digit_in_base(x: Scalar, base: Scalar) -> String = if base < 2 || base > 16 then "?" # TODO: better error handling once we can specify the return type of 'error(msg)' as '!' (see error.nbt). else if mod(x, 16) < 10 then chr(48 + mod(x, 16)) else chr(97 + mod(x, 16) - 10) fn _number_in_base(x: Scalar, b: Scalar) -> String = if x < 0 then "-{_number_in_base(-x, b)}" else if x < b then _digit_in_base(x, b) else str_append(_number_in_base(floor(x / b), b), _digit_in_base(mod(x, b), b)) fn bin(x: Scalar) -> String = if x < 0 then "-{bin(-x)}" else "0b{_number_in_base(x, 2)}" fn oct(x: Scalar) -> String = if x < 0 then "-{oct(-x)}" else "0o{_number_in_base(x, 8)}" fn dec(x: Scalar) -> String = _number_in_base(x, 10) fn hex(x: Scalar) -> String = if x < 0 then "-{hex(-x)}" else "0x{_number_in_base(x, 16)}" # TODO: once we have anonymous functions / closures, we can implement base in a way # that it returns a partially-applied version of '_number_in_base'. This would allow # arbitrary 'x -> base(b)' conversions. fn _not_implemented(unused: Scalar) -> String = "" fn base(b: Scalar) -> Fn[(Scalar) -> String] = if b == 2 then bin else if b == 8 then oct else if b == 10 then dec else if b == 16 then hex else _not_implemented numbat-1.11.0/modules/datetime/functions.nbt000064400000000000000000000014251046102023000171550ustar 00000000000000use core::strings use units::si fn now() -> DateTime fn datetime(input: String) -> DateTime fn format_datetime(format: String, input: DateTime) -> String fn get_local_timezone() -> String fn tz(tz: String) -> Fn[(DateTime) -> DateTime] let local: Fn[(DateTime) -> DateTime] = tz(get_local_timezone()) let UTC: Fn[(DateTime) -> DateTime] = tz("UTC") fn unixtime(input: DateTime) -> Scalar fn from_unixtime(input: Scalar) -> DateTime fn _today_str() = format_datetime("%Y-%m-%d", now()) fn today() -> DateTime = datetime("{_today_str()} 00:00:00") fn date(input: String) -> DateTime = if str_contains(input, " ") then datetime(str_replace(input, " ", " 00:00:00 ")) else datetime("{input} 00:00:00") fn time(input: String) -> DateTime = datetime("{_today_str()} {input}") numbat-1.11.0/modules/datetime/human.nbt000064400000000000000000000027551046102023000162640ustar 00000000000000use core::functions use core::strings use units::si use datetime::functions fn _human_num_days(time: Time) -> Scalar = floor(time / day) fn _human_join(a: String, b: String) -> String = if str_slice(a, 0, 2) == "0 " then b else if str_slice(b, 0, 2) == "0 " then a else "{a} + {b}" fn _remove_plural_suffix(str: String) -> String = if str_slice(str, 0, 2) == "1 " then str_slice(str, 0, str_length(str) - 1) else str fn _human_seconds(dt: DateTime) -> String = _remove_plural_suffix(format_datetime("%-S%.f seconds", dt)) fn _human_minutes(dt: DateTime) -> String = _remove_plural_suffix(format_datetime("%-M minutes", dt)) fn _human_hours(dt: DateTime) -> String = _remove_plural_suffix(format_datetime("%-H hours", dt)) fn _human_days(num_days: Scalar) -> String = _remove_plural_suffix("{num_days} days") fn _human_readable_duration(time: Time, dt: DateTime, num_days: Scalar) -> String = _human_join(_human_join(_human_join(_human_days(_human_num_days(time)), _human_hours(dt)), _human_minutes(dt)), _human_seconds(dt)) # Implementation details: # we skip hours/minutes/seconds for durations larger than 1000 days because: # (a) we run into floating point precision problems at the nanosecond level at this point # (b) for much larger numbers, we can't convert to DateTimes anymore fn human(time: Time) = if _human_num_days(time) > 1000 then "{_human_num_days(time)} days" else _human_readable_duration(time, datetime("0001-01-01T00:00:00Z") + time, _human_num_days(time)) numbat-1.11.0/modules/extra/astronomy.nbt000064400000000000000000000011701046102023000165240ustar 00000000000000use units::si use units::astronomical use physics::constants unit lyr: Length = lightyear @name("Light-second") @url("https://en.wikipedia.org/wiki/Light-second") @aliases(lightseconds, lsec) unit lightsecond: Length = speed_of_light × 1 s unit lunar_mass: Mass = 7.342e22 kg unit lunar_radius: Length = 1737.4 km unit earth_mass: Mass = 5.9722e24 kg unit earth_radius: Length = 6378.1 km unit jupiter_mass: Mass = 1.89813e27 kg unit jupiter_radius: Length = 71_492 km unit solar_mass: Mass = 1.98847e30 kg unit solar_radius: Length = 6.957e5 km dimension RadiantFlux = Power unit solar_luminosity: RadiantFlux = 3.828e26 W numbat-1.11.0/modules/extra/cooking.nbt000064400000000000000000000007411046102023000161250ustar 00000000000000# (Inverse) densities for various cooking ingredients. # # Example usage: # # use cooking # # 200g butter to tablespoons # 500g rice to cups # use units::si let water = 1 / (1000 g/L) let butter = 1 / (911 g/L) let olive_oil = 1 / (920 g/L) let milk = 1 / (1030 g/L) let sugar = 1 / (845 g/L) # Granulated sugar let honey = 1 / (1420 g/L) let flour = 1 / (550 g/L) let salt = 1 / (1217 g/L) let rice = 1 / (785 g/L) let egg_raw = 1 / (1029 g/L) let yogurt = 1 / (1045 g/L) numbat-1.11.0/modules/math/constants.nbt000064400000000000000000000052101046102023000163120ustar 00000000000000use core::scalar ### Mathematical @name("Pi") @url("https://en.wikipedia.org/wiki/Pi") @aliases(pi) let π = 3.14159265358979323846264338327950288 @name("Tau") @url("https://en.wikipedia.org/wiki/Turn_(angle)#Tau_proposals") let τ = 2 π @name("Euler's number") @url("https://en.wikipedia.org/wiki/E_(mathematical_constant)") let e = 2.71828182845904523536028747135266250 @name("Golden ratio") @url("https://en.wikipedia.org/wiki/Golden_ratio") @aliases(golden_ratio) let φ = 1.61803398874989484820458683436563811 ### Named numbers #### Large numbers @name("Hundred") @url("https://en.wikipedia.org/wiki/100_(number)") unit hundred = 100 @name("Thousand") @url("https://en.wikipedia.org/wiki/1000_(number)") unit thousand = 1_000 @name("Million") @url("https://en.wikipedia.org/wiki/Million") unit million = 1_000_000 @name("Billion") @url("https://en.wikipedia.org/wiki/Billion") unit billion = 10^9 @name("Trillion") @url("https://en.wikipedia.org/wiki/Trillion") unit trillion = 10^12 @name("Quadrillion") @url("https://en.wikipedia.org/wiki/Quadrillion") unit quadrillion = 10^15 @name("Quintillion") @url("https://en.wikipedia.org/wiki/Quintillion") unit quintillion = 10^18 @name("Googol") @url("https://en.wikipedia.org/wiki/Googol") let googol = 10^100 ### Unicode fractions @name("One half") @url("https://en.wikipedia.org/wiki/One_half") @aliases(half, semi) let ½ = 1 / 2 let ⅓ = 1 / 3 let ⅔ = 2 / 3 @aliases(quarter) let ¼ = 1 / 4 let ¾ = 3 / 4 let ⅕ = 1 / 5 let ⅖ = 2 / 5 let ⅗ = 3 / 5 let ⅘ = 4 / 5 let ⅙ = 1 / 6 let ⅚ = 5 / 6 let ⅐ = 1 / 7 let ⅛ = 1 / 8 let ⅜ = 3 / 8 let ⅝ = 5 / 8 let ⅞ = 7 / 8 let ⅑ = 1 / 9 let ⅒ = 1 / 10 #### Integers and colloquial names @name("One") @url("https://en.wikipedia.org/wiki/1") let one = 1 @name("Two") @url("https://en.wikipedia.org/wiki/2") @aliases(double) let two = 2 @name("Three") @url("https://en.wikipedia.org/wiki/3") @aliases(triple) let three = 3 @name("Four") @url("https://en.wikipedia.org/wiki/4") @aliases(quadruple) let four = 4 @name("Five") @url("https://en.wikipedia.org/wiki/5") let five = 5 @name("Six") @url("https://en.wikipedia.org/wiki/6") let six = 6 @name("Seven") @url("https://en.wikipedia.org/wiki/7") let seven = 7 @name("Eight") @url("https://en.wikipedia.org/wiki/8") let eight = 8 @name("Nine") @url("https://en.wikipedia.org/wiki/9") let nine = 9 @name("Ten") @url("https://en.wikipedia.org/wiki/10") let ten = 10 @name("Eleven") @url("https://en.wikipedia.org/wiki/11") let eleven = 11 @name("Twelve") @url("https://en.wikipedia.org/wiki/12") let twelve = 12 @name("Dozen") @url("https://en.wikipedia.org/wiki/Dozen") unit dozen = 12 numbat-1.11.0/modules/math/functions.nbt000064400000000000000000000025611046102023000163140ustar 00000000000000use core::scalar use math::constants ## Basics fn sqrt(x: D^2) -> D = x^(1/2) fn sqr(x: D) -> D^2 = x^2 ## Exponential and logarithm fn exp(x: Scalar) -> Scalar fn ln(x: Scalar) -> Scalar fn log(x: Scalar) -> Scalar = ln(x) fn log10(x: Scalar) -> Scalar fn log2(x: Scalar) -> Scalar ## Trigonometry fn sin(x: Scalar) -> Scalar fn cos(x: Scalar) -> Scalar fn tan(x: Scalar) -> Scalar fn asin(x: Scalar) -> Scalar fn acos(x: Scalar) -> Scalar fn atan(x: Scalar) -> Scalar fn atan2(y: T, x: T) -> Scalar fn sinh(x: Scalar) -> Scalar fn cosh(x: Scalar) -> Scalar fn tanh(x: Scalar) -> Scalar fn asinh(x: Scalar) -> Scalar fn acosh(x: Scalar) -> Scalar fn atanh(x: Scalar) -> Scalar # Note: there are even more functions in `math::trigonmetry_extra`. ## Others fn gamma(x: Scalar) -> Scalar ### Statistics fn mean(xs: D…) -> D fn maximum(xs: D…) -> D fn minimum(xs: D…) -> D ### Geometry fn hypot2(x: T, y: T) -> T = sqrt(x^2 + y^2) fn hypot3(x: T, y: T, z: T) -> T = sqrt(x^2 + y^2 + z^2) # The following functions use a generic dimension instead of # 'Length' in order to allow for computations in pixels, for # example fn circle_area(radius: L) -> L^2 = π × radius^2 fn circle_circumference(radius: L) -> L = 2 π × radius fn sphere_area(radius: L) -> L^2 = 4 π × radius^2 fn sphere_volume(radius: L) -> L^3 = 4/3 × π × radius^3 numbat-1.11.0/modules/math/trigonometry_extra.nbt000064400000000000000000000012451046102023000202470ustar 00000000000000use math::functions fn cot(x: Scalar) -> Scalar = 1 / tan(x) fn acot(x: Scalar) -> Scalar = atan(1 / x) fn coth(x: Scalar) -> Scalar = (e^x + e^-x) / (e^x - e^-x) fn acoth(x: Scalar) -> Scalar = 1/2 × ln((x + 1) / (x - 1)) fn secant(x: Scalar) -> Scalar = 1 / cos(x) fn arcsecant(x: Scalar) -> Scalar = acos(1 / x) fn cosecant(x: Scalar) -> Scalar = 1 / sin(x) fn csc(x: Scalar) -> Scalar = cosecant(x) fn acsc(x: Scalar) -> Scalar = asin(1 / x) fn sech(x: Scalar) -> Scalar = 1 / cosh(x) fn asech(x: Scalar) -> Scalar = ln(sqrt(1 / x - 1) sqrt(1 / x + 1) + 1 / x) fn csch(x: Scalar) -> Scalar = 1 / sinh(x) fn acsch(x: Scalar) -> Scalar = ln(sqrt(1 + 1 / x^2) + 1 / x) numbat-1.11.0/modules/physics/constants.nbt000064400000000000000000000105311046102023000170450ustar 00000000000000use math::functions use units::si @name("Speed of light in vacuum") @url("https://en.wikipedia.org/wiki/Speed_of_light") @aliases(c) let speed_of_light: Velocity = 299_792_458 m / s @name("Newtonian constant of gravitation") @url("https://en.wikipedia.org/wiki/Gravitational_constant") @aliases(G) let gravitational_constant: Force × Length^2 / Mass^2 = 6.674_30e-11 m³ / (kg s²) @name("Standard acceleration of gravity on earth") @url("https://en.wikipedia.org/wiki/Gravity_of_Earth") @aliases(g0) let gravity: Acceleration = 9.806_65 m / s² @name("Planck constant") @url("https://en.wikipedia.org/wiki/Planck_constant") @aliases(ℎ) let planck_constant: Action = 6.626_070_15e-34 J / Hz @name("Reduced Planck constant") @url("https://en.wikipedia.org/wiki/Planck_constant#Reduced_Planck_constant_%E2%84%8F") @aliases(h_bar) let ℏ: AngularMomentum = planck_constant / 2π @name("Electron mass") @url("https://en.wikipedia.org/wiki/Electron_mass") let electron_mass: Mass = 9.109_383_701_5e-31 kg @name("Elementary charge") @url("https://en.wikipedia.org/wiki/Elementary_charge") @aliases(electron_charge) let elementary_charge: ElectricCharge = 1.602_176_634e-19 C @name("Vacuum permeability / magnetic constant") @url("https://en.wikipedia.org/wiki/Vacuum_permeability") @aliases(µ0,μ0,mu0) let magnetic_constant: MagneticPermeability = 1.256_637_062_12e-6 N / A² @name("Vacuum electric permittivity / electric constant") @url("https://en.wikipedia.org/wiki/Vacuum_permittivity") @aliases(ε0,eps0) let electric_constant: ElectricPermittivity = 1 / (µ0 c²) -> F/m @name("Bohr magneton") @aliases(µ_B,μ_B) @url("https://en.wikipedia.org/wiki/Bohr_magneton") let bohr_magneton: Energy / MagneticFluxDensity = electron_charge ℏ / 2 electron_mass -> J/T @name("Fine structure constant") @url("https://en.wikipedia.org/wiki/Fine-structure_constant") @aliases(α, alpha) let fine_structure_constant: Scalar = electron_charge^2 / (2 eps0 ℎ c) @name("Proton mass") @url("https://en.wikipedia.org/wiki/Proton") let proton_mass: Mass = 1.672_621_923_69e-27 kg @name("Neutron mass") @url("https://en.wikipedia.org/wiki/Neutron") let neutron_mass: Mass = 1.674_927_498_04e-27 kg @name("Avogadro constant") @url("https://en.wikipedia.org/wiki/Avogadro_constant") @aliases(N_A) let avogadro_constant: 1 / AmountOfSubstance = 6.022_140_76e23 / mol @name("Boltzmann constant") @url("https://en.wikipedia.org/wiki/Boltzmann_constant") @aliases(k_B) let boltzmann_constant: Energy / Temperature = 1.380_649e-23 J / K @name("Stefan-Boltzmann constant") @url("https://en.wikipedia.org/wiki/Stefan%E2%80%93Boltzmann_law") let stefan_boltzmann_constant: Power / (Area × Temperature^4) = 2 π^5 k_B^4 / (15 planck_constant^3 c^2) @name("Molar gas constant") @url("https://en.wikipedia.org/wiki/Gas_constant") @aliases(R) let gas_constant: Energy / (AmountOfSubstance × Temperature) = k_B × N_A @name("Bohr radius") @url("https://en.wikipedia.org/wiki/Bohr_radius") @aliases(a0) let bohr_radius: Length = 4 pi ε0 ℏ^2 / (electron_charge^2 electron_mass) @name("Rydberg constant") @url("https://en.wikipedia.org/wiki/Rydberg_constant") let rydberg_constant: Wavenumber = (electron_mass electron_charge^4) / (8 ε0^2 ℎ^3 c) @name("Rydberg unit of energy") @url("https://en.wikipedia.org/wiki/Rydberg_constant") unit Ry: Energy = ℎ c × rydberg_constant @name("Atomic Mass constant") @url("https://en.wikipedia.org/wiki/Atomic_mass_constant") @aliases(m_u) let atomic_mass_constant: Mass = 1 dalton -> kg @name("Conductance quantum") @url("https://en.wikipedia.org/wiki/Conductance_quantum") let conductance_quantum: ElectricConductance = 2 * elementary_charge^2 / planck_constant -> S @name("Faraday constant") @url("https://en.wikipedia.org/wiki/Faraday_constant") let faraday_constant: ElectricCharge / AmountOfSubstance = avogadro_constant * elementary_charge @name("Magnetic Flux Quantum") @url("https://en.wikipedia.org/wiki/Magnetic_flux_quantum") let magnetic_flux_quantum: MagneticFlux = planck_constant / (2 * elementary_charge) -> Wb @name("Josephson Constant") @url("https://en.wikipedia.org/wiki/Josephson_constant") let josephson_constant: Frequency / Voltage = 1 / magnetic_flux_quantum -> Hz/V @name("Von Klitzing Constant") @url("https://en.wikipedia.org/wiki/Von_Klitzing_constant") @aliases(R_K) let von_klitzing_constant: ElectricResistance = planck_constant / (elementary_charge^2) -> Ω numbat-1.11.0/modules/physics/temperature_conversion.nbt000064400000000000000000000011001046102023000216230ustar 00000000000000use units::si ### Temperature conversion functions K <-> °C and K <-> °F let _offset_celsius = 273.15 fn from_celsius(t_celsius: Scalar) -> Temperature = (t_celsius + _offset_celsius) kelvin fn celsius(t_kelvin: Temperature) -> Scalar = t_kelvin / kelvin - _offset_celsius let _offset_fahrenheit = 459.67 let _scale_fahrenheit = 5 / 9 fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature = ((t_fahrenheit + _offset_fahrenheit) × _scale_fahrenheit) kelvin fn fahrenheit(t_kelvin: Temperature) -> Scalar = (t_kelvin / kelvin) / _scale_fahrenheit - _offset_fahrenheit numbat-1.11.0/modules/prelude.nbt000064400000000000000000000010731046102023000150100ustar 00000000000000use core::scalar use core::quantities use core::dimensions use core::functions use core::strings use core::error use math::constants use math::functions use math::trigonometry_extra use units::si use units::time use units::astronomical use units::imperial use units::us_customary use units::nautical use units::cgs use units::planck use units::fff use units::misc use units::humorous use units::partsperx use units::currency use units::bit use units::placeholder use physics::constants use physics::temperature_conversion use datetime::functions use datetime::human numbat-1.11.0/modules/units/astronomical.nbt000064400000000000000000000007421046102023000172070ustar 00000000000000use units::si @name("Parsec") @url("https://en.wikipedia.org/wiki/Parsec") @metric_prefixes @aliases(parsecs, pc: short) unit parsec: Length = 648_000 / π × au @name("Light-year") @url("https://en.wikipedia.org/wiki/Light-year") @metric_prefixes @aliases(lightyears, ly: short) unit lightyear: Length = 9_460_730_472_580_800 m @name("Sidereal day") @url("https://en.wikipedia.org/wiki/Sidereal_time#Sidereal_day") @aliases(sidereal_days) unit sidereal_day: Time = 86164.0905 s numbat-1.11.0/modules/units/bit.nbt000064400000000000000000000011201046102023000152610ustar 00000000000000use units::si dimension DigitalInformation @name("Bit") @url("https://en.wikipedia.org/wiki/Bit") @metric_prefixes @binary_prefixes @aliases(bit: both, bits: both) unit bit: DigitalInformation @name("Byte") @url("https://en.wikipedia.org/wiki/Byte") @metric_prefixes @binary_prefixes @aliases(B: short, byte: both, bytes: both, Byte: both, Bytes: both, octet, octets, Octet, Octets) unit byte: DigitalInformation = 8 bit @name("Bits per second") @url("https://en.wikipedia.org/wiki/Bit_per_second") @metric_prefixes @aliases(bps: short) unit bps: DigitalInformation / Time = bit / second numbat-1.11.0/modules/units/cgs.nbt000064400000000000000000000016621046102023000152720ustar 00000000000000use units::si ### Centimetre–gram–second system of units @name("Dyne") @url("https://en.wikipedia.org/wiki/Dyne") @aliases(dyn) unit dyne: Force = 1e-5 N @name("Erg") @url("https://en.wikipedia.org/wiki/Erg") @aliases(ergs) unit erg: Energy = 1 dyn cm @name("Gauss") @url("https://en.wikipedia.org/wiki/Gauss_(unit)") unit gauss: MagneticFluxDensity = 100 µT @name("Maxwell") @url("https://en.wikipedia.org/wiki/Maxwell_(unit)") @aliases(Mx) unit maxwell: MagneticFlux = 1 gauss × cm^2 @name("Oersted") @url("https://en.wikipedia.org/wiki/Oersted") @metric_prefixes @aliases(Oe: short) unit oersted: MagneticFieldStrength = 1 / (4 pi) * dyne / maxwell @name("Poise") @url("https://en.wikipedia.org/wiki/Poise_(unit)") @metric_prefixes unit poise: DynamicViscosity = 1 dyn × s / cm^2 @name("Stokes") @url("https://en.wikipedia.org/wiki/Stokes_(unit)") @metric_prefixes @aliases(St: short) unit stokes: KinematicViscosity = cm^2 / s numbat-1.11.0/modules/units/currencies.nbt000064400000000000000000000125431046102023000166600ustar 00000000000000use core::scalar use units::currency # This module is currently not part of the prelude, because the 'exchange_rate("XYZ")' calls # are blocking. For the CLI application, we do however load this module on demand if one of # the identifiers below is. For the Web version, we asynchronously load exchange rates and then # pull in this module. fn exchange_rate(currency: String) -> Scalar @name("US dollar") @url("https://en.wikipedia.org/wiki/United_States_dollar") @aliases(dollars, USD, $: short) unit dollar: Money = EUR / exchange_rate("USD") @name("Japanese yen") @url("https://en.wikipedia.org/wiki/Japanese_yen") @aliases(yens, JPY, ¥: short, 円) unit yen: Money = EUR / exchange_rate("JPY") @name("Pound sterling") @url("https://en.wikipedia.org/wiki/Pound_sterling") @aliases(pound_sterling, GBP, £: short) unit british_pound: Money = EUR / exchange_rate("GBP") @name("Chinese yuan") @url("https://en.wikipedia.org/wiki/Renminbi") @aliases(CNY: short, 元) unit renminbi: Money = EUR / exchange_rate("CNY") @name("Australian dollar") @url("https://en.wikipedia.org/wiki/Australian_dollar") @aliases(australian_dollars, AUD: short, A$) unit australian_dollar: Money = EUR / exchange_rate("AUD") @name("Canadian dollar") @url("https://en.wikipedia.org/wiki/Canadian_dollar") @aliases(canadian_dollars, CAD: short, C$) unit canadian_dollar: Money = EUR / exchange_rate("CAD") @name("Swiss franc") @url("https://en.wikipedia.org/wiki/Swiss_franc") @aliases(swiss_francs, CHF: short) unit swiss_franc: Money = EUR / exchange_rate("CHF") @name("Bulgarian lev") @url("https://en.wikipedia.org/wiki/Bulgarian_lev") @aliases(bulgarian_leva, BGN: short) unit bulgarian_lev: Money = EUR / exchange_rate("BGN") @name("Czech koruna") @url("https://en.wikipedia.org/wiki/Czech_koruna") @aliases(czech_korunas, CZK: short, Kč) unit czech_koruna: Money = EUR / exchange_rate("CZK") @name("Hungarian forint") @url("https://en.wikipedia.org/wiki/Hungarian_forint") @aliases(hungarian_forints, HUF: short, Ft) unit hungarian_forint: Money = EUR / exchange_rate("HUF") @name("Polish złoty") @url("https://en.wikipedia.org/wiki/Polish_złoty") @aliases(polish_zlotys, PLN: short, zł) unit polish_zloty: Money = EUR / exchange_rate("PLN") @name("Romanian leu") @url("https://en.wikipedia.org/wiki/Romanian_leu") @aliases(romanian_leus, RON: short, lei) unit romanian_leu: Money = EUR / exchange_rate("RON") @name("Turkish lira") @url("https://en.wikipedia.org/wiki/Turkish_lira") @aliases(turkish_liras, TRY: short, ₺) unit turkish_lira: Money = EUR / exchange_rate("TRY") @name("Brazilian real") @url("https://en.wikipedia.org/wiki/Brazilian_real") @aliases(brazilian_reals, BRL: short, R$) unit brazilian_real: Money = EUR / exchange_rate("BRL") @name("Hong Kong dollar") @url("https://en.wikipedia.org/wiki/Hong_Kong_dollar") @aliases(hong_kong_dollars, HKD: short, HK$) unit hong_kong_dollar: Money = EUR / exchange_rate("HKD") @name("Indonesian rupiah") @url("https://en.wikipedia.org/wiki/Indonesian_rupiah") @aliases(indonesian_rupiahs, IDR: short, Rp) unit indonesian_rupiah: Money = EUR / exchange_rate("IDR") @name("Indian rupee") @url("https://en.wikipedia.org/wiki/Indian_rupee") @aliases(indian_rupees, INR: short, ₹) unit indian_rupee: Money = EUR / exchange_rate("INR") @name("South Korean won") @url("https://en.wikipedia.org/wiki/South_Korean_won") @aliases(south_korean_wons, KRW: short, ₩) unit south_korean_won: Money = EUR / exchange_rate("KRW") @name("Malaysian ringgit") @url("https://en.wikipedia.org/wiki/Malaysian_ringgit") @aliases(malaysian_ringgits, MYR: short, RM) unit malaysian_ringgit: Money = EUR / exchange_rate("MYR") @name("New Zealand dollar") @url("https://en.wikipedia.org/wiki/New_Zealand_dollar") @aliases(new_zealand_dollars, NZD: short, NZ$) unit new_zealand_dollar: Money = EUR / exchange_rate("NZD") @name("Philippine peso") @url("https://en.wikipedia.org/wiki/Philippine_peso") @aliases(philippine_pesos, PHP: short, ₱) unit philippine_peso: Money = EUR / exchange_rate("PHP") @name("Singapore dollar") @url("https://en.wikipedia.org/wiki/Singapore_dollar") @aliases(singapore_dollars, SGD: short, S$) unit singapore_dollar: Money = EUR / exchange_rate("SGD") @name("Thai baht") @url("https://en.wikipedia.org/wiki/Thai_baht") @aliases(thai_bahts, THB: short, ฿) unit thai_baht: Money = EUR / exchange_rate("THB") @name("Danish krone") @url("https://en.wikipedia.org/wiki/Danish_krone") @aliases(danish_kroner, DKK: short) unit danish_krone: Money = EUR / exchange_rate("DKK") @name("Swedish krona") @url("https://en.wikipedia.org/wiki/Swedish_krona") @aliases(swedish_kronor, SEK: short) unit swedish_krona: Money = EUR / exchange_rate("SEK") @name("Icelandic króna") @url("https://en.wikipedia.org/wiki/Icelandic_króna") @aliases(icelandic_krónur, icelandic_krona, icelandic_kronur, ISK: short) unit icelandic_króna: Money = EUR / exchange_rate("ISK") @name("Norwegian krone") @url("https://en.wikipedia.org/wiki/Norwegian_krone") @aliases(norwegian_kroner, NOK: short) unit norwegian_krone: Money = EUR / exchange_rate("NOK") @name("Israeli new shekel") @url("https://en.wikipedia.org/wiki/Israeli_new_shekel") @aliases(israeli_new_shekels, ILS: short, ₪, NIS) unit israeli_new_shekel: Money = EUR / exchange_rate("ILS") @name("South African rand") @url("https://en.wikipedia.org/wiki/South_African_rand") @aliases(ZAR: short) unit south_african_rand: Money = EUR / exchange_rate("ZAR") numbat-1.11.0/modules/units/currency.nbt000064400000000000000000000002531046102023000163430ustar 00000000000000dimension Money @name("Euro") @url("https://en.wikipedia.org/wiki/Euro") @aliases(euros, EUR, €: short) unit euro: Money # See currencies.nbt for non-Euro currencies numbat-1.11.0/modules/units/fff.nbt000064400000000000000000000007711046102023000152570ustar 00000000000000use units::imperial # The furlong–firkin–fortnight system # https://en.wikipedia.org/wiki/FFF_system @name("Furlong") @url("https://en.wikipedia.org/wiki/Furlong") @metric_prefixes @aliases(furlongs) unit furlong: Length = 220 yard @name("Firkin") @url("https://en.wikipedia.org/wiki/Firkin_(unit)") @metric_prefixes @aliases(firkins) unit firkin: Mass = 90 lb @name("Fortnight") @url("https://en.wikipedia.org/wiki/Fortnight") @metric_prefixes @aliases(fortnights) unit fortnight: Time = 14 days numbat-1.11.0/modules/units/hartree.nbt000064400000000000000000000003771046102023000161520ustar 00000000000000use physics::constants @name("Hartree") @url("https://en.wikipedia.org/wiki/Hartree") @aliases(hartrees) unit hartree: Energy = ℏ^2 / (electron_mass a0^2) @name("Bohr") @url("https://en.wikipedia.org/wiki/Hartree_atomic_units") unit bohr: Length = a0 numbat-1.11.0/modules/units/humorous.nbt000064400000000000000000000002031046102023000163650ustar 00000000000000use units::si use units::imperial @name("Smoot") @url("https://en.wikipedia.org/wiki/Smoot") unit smoot: Length = 5 feet + 7 inch numbat-1.11.0/modules/units/imperial.nbt000064400000000000000000000060221046102023000163130ustar 00000000000000use units::si use units::misc ### Imperial unit system @name("Inch") @url("https://en.wikipedia.org/wiki/Inch") @aliases(inches, in: short) unit inch: Length = 0.0254 meter @name("Foot") @url("https://en.wikipedia.org/wiki/Foot_(unit)") @aliases(feet, ft: short) unit foot: Length = 12 inch @name("Yard") @url("https://en.wikipedia.org/wiki/Yard") @aliases(yards, yd: short) unit yard: Length = 3 feet @name("Mile") @url("https://en.wikipedia.org/wiki/Mile") @aliases(miles, mi: short) unit mile: Length = 1760 yard @name("Thousandth of an inch") @url("https://en.wikipedia.org/wiki/Thousandth_of_an_inch") @aliases(mils, mil: short) unit thou: Length = inch / 1000 @name("Fathom") @url("https://en.wikipedia.org/wiki/Fathom") @aliases(fathoms) unit fathom: Length = 2 yard @name("League") @url("https://en.wikipedia.org/wiki/League_(unit)") @aliases(leagues) unit league: Length = 3 mile @name("Grain") @url("https://en.wikipedia.org/wiki/Grain_(unit)") @aliases(grains) unit grain: Mass = 64.79891 milligram @name("Pound") @url("https://en.wikipedia.org/wiki/Pound_(mass)") @aliases(pounds, lb: short, lbs) unit pound: Mass = 7000 grain @name("Ounce") @url("https://en.wikipedia.org/wiki/Ounce") @aliases(ounces, oz: short) unit ounce: Mass = (1 / 16) × pound @name("Stone") @url("https://en.wikipedia.org/wiki/Stone_(unit)") unit stone: Mass = 14 pound @name("Hundredweight") @url("https://en.wikipedia.org/wiki/Hundredweight") @aliases(cwt) unit long_hundredweight = 8 stone @name("Long ton") @url("https://en.wikipedia.org/wiki/Long_ton") @aliases(long_tons) unit long_ton: Mass = 2240 lb @name("Miles per hour") @url("https://en.wikipedia.org/wiki/Miles_per_hour") unit mph: Velocity = miles per hour @name("Inch of mercury") @url("https://en.wikipedia.org/wiki/Inch_of_mercury") unit inHg: Pressure = in Hg @name("Imperial Fluid Ounce") @url("https://en.wikipedia.org/wiki/Fluid_ounce") @aliases(imperial_fluidounces, UK_floz: short) unit imperial_fluidounce: Volume = 28.4130625 mL @name("Imperial Pint") @url("https://en.wikipedia.org/wiki/Pint#Imperial_pint") @aliases(imperial_pints, UK_pt: short) unit imperial_pint: Volume = 20 imperial_fluidounces @name("Imperial Quart") @url("https://en.wikipedia.org/wiki/Quart#Imperial_quart") @aliases(imperial_quarts, UK_qt: short) unit imperial_quart: Volume = 2 imperial_pints @name("Imperial Gallon") @url("https://en.wikipedia.org/wiki/Gallon#Imperial_gallon") @aliases(imperial_gallons, UK_gal: short) unit imperial_gallon: Volume = 4 imperial_quart @name("Imperial Bushel") @url("https://en.wikipedia.org/wiki/Bushel#Imperial_bushel") @aliases(imperial_bushels, UK_bu: short) unit imperial_bushel: Volume = 8 imperial_gallons @name("Imperial Fluid Drachm") @url("https://en.wikipedia.org/wiki/Fluid_drachm#Imperial_fluid_drachm") @aliases(imperial_fluid_drachms, UK_fldr: short) unit imperial_fluid_drachm: Volume = 1/8 × imperial_fluidounce @name("Imperial Gill") @url("https://en.wikipedia.org/wiki/Gill_(unit)") @aliases(imperial_gills, UK_gi: short) unit imperial_gill: Volume = 5 imperial_fluidounces numbat-1.11.0/modules/units/misc.nbt000064400000000000000000000102461046102023000154470ustar 00000000000000use units::si ### Other units @name("Bar") @url("https://en.wikipedia.org/wiki/Bar_(unit)") @metric_prefixes @aliases(bar: both, bars: both) unit bar: Pressure = 100 kPa @name("Ångström") @url("https://en.wikipedia.org/wiki/Angstrom") @aliases(angstroms, Å: short, Å: short) unit angstrom: Length = 1e-10 meter @name("Barn") @url("https://en.wikipedia.org/wiki/Barn_(unit)") @metric_prefixes @aliases(barns) unit barn: Area = 1e-28 meter^2 @name("Calorie") @url("https://en.wikipedia.org/wiki/Calorie") @metric_prefixes @aliases(calories, cal: both) unit calorie: Energy = 4.184 joule @name("British thermal unit") @url("https://en.wikipedia.org/wiki/British_thermal_unit") @aliases(Btu) unit BTU: Energy = 1055.05585262 joule @name("Pound-force") @url("https://en.wikipedia.org/wiki/Pound_(force)") @aliases(lbf: short) unit pound_force: Force = 4.448222 newton @name("Ounce-force") @url("https://en.wikipedia.org/wiki/Ounce-force") @aliases(ozf: short) unit ounce_force: Force = 1 / 16 * lbf @name("Kilogram-force") @url("https://en.wikipedia.org/wiki/Kilogram-force") @aliases(kgf: short) unit kilogram_force: Force = 9.80665 newton @name("Metric horsepower") @url("https://en.wikipedia.org/wiki/Horsepower") @aliases(hp: short) unit horsepower: Power = 735.49875 W @name("Revolution") @url("https://en.wikipedia.org/wiki/Revolution_(unit)") @aliases(revolutions, rev: short) unit revolution: Angle = 360° @name("Revolutions per minute") @url("https://en.wikipedia.org/wiki/Revolutions_per_minute") @aliases(RPM: short) unit rpm: Frequency = 1 / minute @name("Millimeter of mercury") @url("https://en.wikipedia.org/wiki/Millimeter_of_mercury") unit mmHg: Pressure = 133.322387415 pascal @name("Mercury") @url("https://en.wikipedia.org/wiki/Mercury_(element)") unit Hg: Force / Volume = mmHg / mm @name("Torr") @url("https://en.wikipedia.org/wiki/Torr") unit torr: Pressure = 101325 / 760 × pascal @name("Pound-force per square inch") @url("https://en.wikipedia.org/wiki/Pounds_per_square_inch") @aliases(PSI: short) unit psi: Pressure = 6.894757 kPa @name("Kilopound-force per square inch") @url("https://en.wikipedia.org/wiki/Ksi_(unit)") @aliases(KSI: short) unit ksi: Pressure = 1000 psi @name("Megapound-force per square inch") @url("https://en.wikipedia.org/wiki/Ksi_(unit)") @aliases(MPSI: short) unit mpsi: Pressure = 1000000 psi @name("Standard atmosphere") @url("https://en.wikipedia.org/wiki/Standard_atmosphere_(unit)") @aliases(atmospheres, atm: short) unit atmosphere: Pressure = 101_325 pascal @name("Molar") @url("https://en.wikipedia.org/wiki/Molar_concentration") @metric_prefixes unit molar: Molarity = 1 mol / litre @name("Molal") @url("https://en.wikipedia.org/wiki/Molality") @metric_prefixes unit molal: Molality = 1 mole / kilogram @name("Football field") @url("https://en.wikipedia.org/wiki/Football_pitch") unit footballfield: Area = 105 m × 68 m # Standard FIFA football pitch @name("Swimming pool") @url("https://en.wikipedia.org/wiki/Olympic-size_swimming_pool") unit swimmingpool: Volume = 50 m × 25 m × 2 m # Olympic-size swimming pool (FR3) @name("Rack unit") @url("https://en.wikipedia.org/wiki/Rack_unit") @aliases(rackunits, RU: short, U: short) unit rackunit: Length = 0.04445 meter # Angles @name("Turn") @url("https://en.wikipedia.org/wiki/Turn_(geometry)") @aliases(turns) unit turn: Angle = 2 π rad @name("Gradian") @url("https://en.wikipedia.org/wiki/Gradian") @aliases(gradians, grad, grads, grade, grades, gon, gons) unit gradian: Angle = 90° / 100 ### Abbreviations @name("Watt-hour") @url("https://en.wikipedia.org/wiki/Watt_hour") @metric_prefixes @aliases(Wh: short) unit watthour: Energy = W h @name("Ampere-hour") @url("https://en.wikipedia.org/wiki/Ampere_hour") @metric_prefixes @aliases(Ah: short) unit amperehour: ElectricCharge = A h @name("Kilometres per hour") @url("https://en.wikipedia.org/wiki/Kilometres_per_hour") unit kph: Velocity = kilometer per hour @name("Micron") @url("https://en.wikipedia.org/wiki/Micrometre") unit micron: Length = µm @name("Cubic centimetre") @url("https://en.wikipedia.org/wiki/Cubic_centimetre") @aliases(ccm) unit cc: Volume = cm^3 @name("Fermi") @url("https://en.wikipedia.org/wiki/Femtometre") unit fermi: Length = 1 fm numbat-1.11.0/modules/units/nautical.nbt000064400000000000000000000004711046102023000163130ustar 00000000000000use units::si @name("Knot") @url("https://en.wikipedia.org/wiki/Knot_(unit)") @aliases(knots, kn: short, kt: short) unit knot: Velocity = 463 m / 900 s @name("Nautical Mile") @url("https://en.wikipedia.org/wiki/Nautical_mile") @aliases(nautical_miles, NM: short, nmi: short) unit nautical_mile: Length = 1852 m numbat-1.11.0/modules/units/partsperx.nbt000064400000000000000000000011711046102023000165410ustar 00000000000000@name("Percent") @url("https://en.wikipedia.org/wiki/Percentage") @aliases(%: short, pct) unit percent = 1e-02 @name("Parts per million") @url("https://en.wikipedia.org/wiki/Parts-per_notation") @aliases(ppm) unit partspermillion = 1e-06 @name("Parts per billion") @url("https://en.wikipedia.org/wiki/Parts-per_notation") @aliases(ppb) unit partsperbillion = 1e-09 @name("Parts per trillion") @url("https://en.wikipedia.org/wiki/Parts-per_notation") @aliases(ppt) unit partspertrillion = 1e-12 @name("Parts per quadrillion") @url("https://en.wikipedia.org/wiki/Parts-per_notation") @aliases(ppq) unit partsperquadrillion = 1e-15 numbat-1.11.0/modules/units/placeholder.nbt000064400000000000000000000025621046102023000170000ustar 00000000000000use units::imperial # Smallest addressable element on a digital display dimension Pixel @name("Pixel") @url("https://en.wikipedia.org/wiki/Pixel") @metric_prefixes @aliases(pixels, px: short) unit pixel: Pixel @name("Pixels per inch") @url("https://en.wikipedia.org/wiki/Pixels_per_inch") unit ppi: Pixel / Length = pixel / inch # Smallest possible output resolution on a printing device dimension Dot @name("Dot") @url("https://en.wikipedia.org/wiki/Dots_per_inch") @aliases(dots) unit dot: Dot @name("Dots per inch") @url("https://en.wikipedia.org/wiki/Dots_per_inch") unit dpi: Dot / Length = dots / inch # A single image in a (video) sequence dimension Frame @name("Frame") @url("https://en.wikipedia.org/wiki/Frame_rate") @aliases(frames) unit frame: Frame @name("Frames per second") @url("https://en.wikipedia.org/wiki/Frame_rate") unit fps: Frame / Time = frame / second # Basic unit of time in music dimension Beat @name("Beat") @url("https://en.wikipedia.org/wiki/Beat_(music)") @aliases(beats) unit beat: Beat @name("Beats per minute") @url("https://en.wikipedia.org/wiki/Tempo") @aliases(BPM: short) unit bpm: Beat / Time = beat / minute # A separate or limited portion or quantity of something dimension Piece @name("Piece") @aliases(pieces) unit piece: Piece # A human being dimension Person @name("Person") @aliases(persons, people, capita) unit person: Person numbat-1.11.0/modules/units/planck.nbt000064400000000000000000000013201046102023000157550ustar 00000000000000# https://en.wikipedia.org/wiki/Planck_units use physics::constants @name("Planck length") @url("https://en.wikipedia.org/wiki/Planck_length") unit planck_length: Length = sqrt(ℏ G / c^3) -> m @name("Planck mass") @url("https://en.wikipedia.org/wiki/Planck_mass") unit planck_mass: Mass = sqrt(ℏ c / G) -> kg @name("Planck time") @url("https://en.wikipedia.org/wiki/Planck_time") unit planck_time: Time = sqrt(ℏ G / c^5) -> s @name("Planck temperature") @url("https://en.wikipedia.org/wiki/Planck_temperature") unit planck_temperature: Temperature = sqrt(ℏ c^5 / (G k_B^2)) -> K @name("Planck energy") @url("https://en.wikipedia.org/wiki/Planck_energy") unit planck_energy: Energy = sqrt(ℏ c^5 / G) -> J numbat-1.11.0/modules/units/si.nbt000064400000000000000000000137131046102023000151310ustar 00000000000000use core::dimensions use math::constants ### SI base units @name("Metre") @url("https://en.wikipedia.org/wiki/Metre") @metric_prefixes @aliases(metres, meter, meters, m: short) unit metre: Length @name("Second") @url("https://en.wikipedia.org/wiki/Second") @metric_prefixes @aliases(seconds, s: short, sec: none) unit second: Time @name("Gram") @url("https://en.wikipedia.org/wiki/Gram") @metric_prefixes @aliases(grams, gramme, grammes, g: short) unit gram: Mass @name("Ampere") @url("https://en.wikipedia.org/wiki/Ampere") @metric_prefixes @aliases(amperes, A: short) unit ampere: Current @name("Kelvin") @url("https://en.wikipedia.org/wiki/Kelvin") @metric_prefixes @aliases(kelvins, K: short) unit kelvin: Temperature @name("Mole") @url("https://en.wikipedia.org/wiki/Mole_(unit)") @metric_prefixes @aliases(moles, mol: short) unit mole: AmountOfSubstance @name("Candela") @url("https://en.wikipedia.org/wiki/Candela") @metric_prefixes @aliases(candelas, cd: short) unit candela: LuminousIntensity ### SI derived units @name("Radian") @url("https://en.wikipedia.org/wiki/Radian") @metric_prefixes @aliases(radians, rad: short) unit radian: Angle = meter / meter @name("Steradian") @url("https://en.wikipedia.org/wiki/Steradian") @metric_prefixes @aliases(steradians, sr: short) unit steradian: SolidAngle = radian^2 @name("Hertz") @url("https://en.wikipedia.org/wiki/Hertz") @metric_prefixes @aliases(Hz: short) unit hertz: Frequency = 1 / second @name("Newton") @url("https://en.wikipedia.org/wiki/Newton_(unit)") @metric_prefixes @aliases(newtons, N: short) unit newton: Force = kilogram meter / second^2 @name("Pascal") @url("https://en.wikipedia.org/wiki/Pascal_(unit)") @metric_prefixes @aliases(pascals, Pa: short) unit pascal: Pressure = newton / meter^2 @name("Joule") @url("https://en.wikipedia.org/wiki/Joule") @metric_prefixes @aliases(joules, J: short) unit joule: Energy = newton meter @name("Watt") @url("https://en.wikipedia.org/wiki/Watt") @metric_prefixes @aliases(watts, W: short) unit watt: Power = joule / second @name("Coulomb") @url("https://en.wikipedia.org/wiki/Coulomb") @metric_prefixes @aliases(coulombs, C: short) unit coulomb: ElectricCharge = ampere second @name("Volt") @url("https://en.wikipedia.org/wiki/Volt") @metric_prefixes @aliases(volts, V: short) unit volt: Voltage = kilogram meter^2 / (second^3 ampere) @name("Farad") @url("https://en.wikipedia.org/wiki/Farad") @metric_prefixes @aliases(farads, F: short) unit farad: Capacitance = coulomb / volt @name("Ohm") @url("https://en.wikipedia.org/wiki/Ohm") @metric_prefixes @aliases(ohms, Ω: short, Ω: short) unit ohm: ElectricResistance = volt / ampere @name("Siemens") @url("https://en.wikipedia.org/wiki/Siemens_(unit)") @metric_prefixes @aliases(S: short) unit siemens: ElectricConductance = 1 / ohm @name("Weber") @url("https://en.wikipedia.org/wiki/Weber_(unit)") @metric_prefixes @aliases(webers, Wb: short) unit weber: MagneticFlux = volt second @name("Tesla") @url("https://en.wikipedia.org/wiki/Tesla_(unit)") @metric_prefixes @aliases(teslas, T: short) unit tesla: MagneticFluxDensity = weber / meter^2 @name("Henry") @url("https://en.wikipedia.org/wiki/Henry_(unit)") @metric_prefixes @aliases(henrys, henries, H: short) unit henry: Inductance = weber / ampere @name("Lumen") @url("https://en.wikipedia.org/wiki/Lumen_(unit)") @metric_prefixes @aliases(lumens, lm: short) unit lumen: LuminousFlux = candela steradian @name("Lux") @url("https://en.wikipedia.org/wiki/Lux") @metric_prefixes @aliases(lx: short) unit lux: Illuminance = lumen / meter^2 @name("Becquerel") @url("https://en.wikipedia.org/wiki/Becquerel") @metric_prefixes @aliases(becquerels, Bq: short) unit becquerel: Activity = 1 / second @name("Gray") @url("https://en.wikipedia.org/wiki/Gray_(unit)") @metric_prefixes @aliases(grays, Gy: short) unit gray: AbsorbedDose = joule / kilogram @name("Sievert") @url("https://en.wikipedia.org/wiki/Sievert") @metric_prefixes @aliases(sieverts, Sv: short) unit sievert: EquivalentDose = joule / kilogram @name("Katal") @url("https://en.wikipedia.org/wiki/Katal") @metric_prefixes @aliases(katals, kat: short) unit katal: CatalyticActivity = mole / second ### SI accepted units @name("Minute") @url("https://en.wikipedia.org/wiki/Minute") @aliases(minutes, min: short) unit minute: Time = 60 seconds @name("Hour") @url("https://en.wikipedia.org/wiki/Hour") @aliases(hours, hr, h: short) unit hour: Time = 60 minutes @name("Day") @url("https://en.wikipedia.org/wiki/Day") @aliases(days, day: short, d: short) unit day: Time = 24 hours @name("Astronomical unit") @url("https://en.wikipedia.org/wiki/Astronomical_unit") @aliases(astronomicalunits, au: short, AU: short) unit astronomicalunit: Length = 149_597_870_700 meter @name("Degree") @url("https://en.wikipedia.org/wiki/Degree_(angle)") @aliases(degrees, deg, °: short) unit degree: Angle = π / 180 × radian @name("Minute of arc") @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc") @aliases(arcminutes, arcmin) unit arcminute: Angle = 1 / 60 × degree @name("Second of arc") @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc") @aliases(arcseconds, arcsec) unit arcsecond: Angle = 1 / 60 × arcminute @name("Are") @url("https://en.wikipedia.org/wiki/Are_(unit)") unit are: Area = (10 m)^2 @name("Hectare") @url("https://en.wikipedia.org/wiki/Hectare") @aliases(hectares, ha: short) unit hectare: Area = 100 are @name("Litre") @url("https://en.wikipedia.org/wiki/Litre") @metric_prefixes @aliases(litres, liter, liters, l: short, L: short) unit litre: Volume = decimeter^3 @name("Tonne") @url("https://en.wikipedia.org/wiki/Tonne") @metric_prefixes @aliases(tonnes, ton: both, tons: both, metricton: none) unit tonne: Mass = 10^3 kilogram @name("Dalton") @url("https://en.wikipedia.org/wiki/Dalton") @aliases(daltons, Da: short) unit dalton: Mass = 1.660_539_066_60e-27 kilogram @name("Electron volt") @url("https://en.wikipedia.org/wiki/Electronvolt") @metric_prefixes @aliases(electronvolts, eV: short) unit electronvolt: Energy = 1.602_176_634e-19 joule numbat-1.11.0/modules/units/stoney.nbt000064400000000000000000000007001046102023000160270ustar 00000000000000use physics::constants @name("Stoney length") @url("https://en.wikipedia.org/wiki/Stoney_units") unit stoney_length: Length = sqrt(G × electron_charge^2 / 4 π ε0 c^4) @name("Stoney mass") @url("https://en.wikipedia.org/wiki/Stoney_units") unit stoney_mass: Mass = sqrt(electron_charge^2 / 4 π ε0 G) @name("Stoney time") @url("https://en.wikipedia.org/wiki/Stoney_units") unit stoney_time: Time = sqrt(G × electron_charge^2 / 4 π ε0 c^6) numbat-1.11.0/modules/units/time.nbt000064400000000000000000000033641046102023000154550ustar 00000000000000use units::si @name("Week") @url("https://en.wikipedia.org/wiki/Week") @aliases(weeks) unit week: Time = 7 days # The mean tropical year changes over time (half a second per century). It's current # value can be approximated using # # 365.2421896698 - 6.15359e-6 T - 7.29e-10 T^2 + 2.64e-10 T^3 # # where T is in Julian centuries, measured from noon January 1st, 2000. # (https://en.wikipedia.org/wiki/Tropical_year#Mean_tropical_year_current_value) # # Values of the mean tropical year for the recent past and near future: # # Year Length (days) # --------------------- # 2020 365.242 189 7 # 2025 365.242 188 1 # 2050 365.242 186 6 # # For now, we use the 2025 value as a hardcoded constant. Those numbers # are mainly shown to illustrate that it is not sensible to define this # number more precise. # @name("Tropical year") @url("https://en.wikipedia.org/wiki/Tropical_year") @metric_prefixes @aliases(years, yr: short, tropical_year, tropical_years) unit year: Time = 365.242_188_1 days @name("Month") @url("https://en.wikipedia.org/wiki/Month") @aliases(months) unit month: Time = year / 12 @name("Gregorian year") @url("https://en.wikipedia.org/wiki/Gregorian_year") @metric_prefixes @aliases(gregorian_years) unit gregorian_year: Time = 365.2425 days @name("Julian year") @url("https://en.wikipedia.org/wiki/Julian_year_(astronomy)") @aliases(julian_years) unit julian_year: Time = 365.25 days @name("Decade") @url("https://en.wikipedia.org/wiki/Decade") @aliases(decades) unit decade: Time = 10 years @name("Century") @url("https://en.wikipedia.org/wiki/Century") @aliases(centuries) unit century: Time = 100 years @name("Millennium") @url("https://en.wikipedia.org/wiki/Millennium") @aliases(millennia) unit millennium: Time = 1000 years numbat-1.11.0/modules/units/us_customary.nbt000064400000000000000000000026541046102023000172550ustar 00000000000000use units::si use units::imperial @name("US liquid gallon") @url("https://en.wikipedia.org/wiki/Gallon") @aliases(gallons, gal: short) unit gallon: Volume = 231 in^3 @name("US liquid pint") @url("https://en.wikipedia.org/wiki/Pint") @aliases(pints) unit pint: Volume = 1/8 × gallon @name("US cup") @url("https://en.wikipedia.org/wiki/Cup_(unit)") @aliases(cups) unit cup: Volume = 1/2 × pint @name("US tablespoon") @url("https://en.wikipedia.org/wiki/Tablespoon") @aliases(tablespoons, tbsp) unit tablespoon: Volume = 1/16 × cup @name("US teaspoon") @url("https://en.wikipedia.org/wiki/Teaspoon") @aliases(teaspoons, tsp) unit teaspoon: Volume = 1/3 × tablespoon @name("US fluid ounce") @url("https://en.wikipedia.org/wiki/Fluid_ounce") @aliases(fluidounces, floz: short) unit fluidounce: Volume = 2 tablespoon @name("US hogshead") @url("https://en.wikipedia.org/wiki/Hogshead") @aliases(hogsheads) unit hogshead: Volume = 63 gallon @name("US rod") @url("https://en.wikipedia.org/wiki/Rod_(unit)") @aliases(rods, perch) unit rod: Length = 16.5 ft @name("Acre") @url("https://en.wikipedia.org/wiki/Acre") @aliases(acres) unit acre: Area = 4840 yard^2 @name("Miles per gallon") @url("https://en.wikipedia.org/wiki/Fuel_economy_in_automobiles") unit mpg: Length / Volume = miles per gallon @name("Foot-candle") @url("https://en.wikipedia.org/wiki/Foot-candle") @aliases(footcandles, fc: short) unit footcandle: Illuminance = lumen / foot^2 numbat-1.11.0/src/arithmetic.rs000064400000000000000000000021561046102023000144640ustar 00000000000000use num_rational::Ratio; use num_traits::Signed; pub type Rational = Ratio; pub type Exponent = Rational; pub trait Power { fn power(self, e: Exponent) -> Self; fn invert(self) -> Self where Self: Sized, { self.power(Exponent::from_integer(-1)) } } pub fn pretty_exponent(e: &Exponent) -> String { if e == &Ratio::from_integer(5) { "⁵".into() } else if e == &Ratio::from_integer(4) { "⁴".into() } else if e == &Ratio::from_integer(3) { "³".into() } else if e == &Ratio::from_integer(2) { "²".into() } else if e == &Ratio::from_integer(1) { "".into() } else if e == &Ratio::from_integer(-1) { "⁻¹".into() } else if e == &Ratio::from_integer(-2) { "⁻²".into() } else if e == &Ratio::from_integer(-3) { "⁻³".into() } else if e == &Ratio::from_integer(-4) { "⁻⁴".into() } else if e == &Ratio::from_integer(-5) { "⁻⁵".into() } else if e.is_positive() && e.is_integer() { format!("^{}", e) } else { format!("^({})", e) } } numbat-1.11.0/src/ast.rs000064400000000000000000000454101046102023000131220ustar 00000000000000use crate::markup as m; use crate::span::Span; use crate::{ arithmetic::Exponent, decorator::Decorator, markup::Markup, number::Number, prefix::Prefix, pretty_print::PrettyPrint, resolver::ModulePath, }; use itertools::Itertools; use num_traits::Signed; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnaryOperator { Factorial, Negate, LogicalNeg, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BinaryOperator { Add, Sub, Mul, Div, Power, ConvertTo, LessThan, GreaterThan, LessOrEqual, GreaterOrEqual, Equal, NotEqual, LogicalAnd, LogicalOr, } impl PrettyPrint for BinaryOperator { fn pretty_print(&self) -> Markup { use BinaryOperator::*; match self { Add => m::space() + m::operator("+") + m::space(), Sub => m::space() + m::operator("-") + m::space(), Mul => m::space() + m::operator("×") + m::space(), Div => m::space() + m::operator("/") + m::space(), Power => m::operator("^"), ConvertTo => m::space() + m::operator("➞") + m::space(), LessThan => m::space() + m::operator("<") + m::space(), GreaterThan => m::space() + m::operator(">") + m::space(), LessOrEqual => m::space() + m::operator("≤") + m::space(), GreaterOrEqual => m::space() + m::operator("≥") + m::space(), Equal => m::space() + m::operator("==") + m::space(), NotEqual => m::space() + m::operator("≠") + m::space(), LogicalAnd => m::space() + m::operator("&&") + m::space(), LogicalOr => m::space() + m::operator("||") + m::space(), } } } #[derive(Debug, Clone, PartialEq)] pub enum StringPart { Fixed(String), Interpolation(Span, Box), } #[derive(Debug, Clone, PartialEq)] pub enum Expression { Scalar(Span, Number), Identifier(Span, String), UnitIdentifier(Span, Prefix, String, String), UnaryOperator { op: UnaryOperator, expr: Box, span_op: Span, }, BinaryOperator { op: BinaryOperator, lhs: Box, rhs: Box, span_op: Option, // not available for implicit multiplication and unicode exponents }, FunctionCall(Span, Span, Box, Vec), Boolean(Span, bool), String(Span, Vec), Condition(Span, Box, Box, Box), } impl Expression { pub fn full_span(&self) -> Span { match self { Expression::Scalar(span, _) => *span, Expression::Identifier(span, _) => *span, Expression::UnitIdentifier(span, _, _, _) => *span, Expression::UnaryOperator { op: _, expr, span_op, } => span_op.extend(&expr.full_span()), Expression::BinaryOperator { op: _, lhs, rhs, span_op, } => { let mut span = lhs.full_span().extend(&rhs.full_span()); if let Some(span_op) = span_op { span = span.extend(span_op); } span } Expression::FunctionCall(_identifier_span, full_span, _, _) => *full_span, Expression::Boolean(span, _) => *span, Expression::Condition(span_if, _, _, then_expr) => { span_if.extend(&then_expr.full_span()) } Expression::String(span, _) => *span, } } } #[cfg(test)] macro_rules! scalar { ( $num:expr ) => {{ crate::ast::Expression::Scalar(Span::dummy(), Number::from_f64($num)) }}; } #[cfg(test)] macro_rules! identifier { ( $name:expr ) => {{ crate::ast::Expression::Identifier(Span::dummy(), $name.into()) }}; } #[cfg(test)] macro_rules! boolean { ( $name:expr ) => {{ crate::ast::Expression::Boolean(Span::dummy(), $name.into()) }}; } #[cfg(test)] macro_rules! logical_neg { ( $rhs:expr ) => {{ crate::ast::Expression::UnaryOperator { op: UnaryOperator::LogicalNeg, expr: Box::new($rhs), span_op: Span::dummy(), } }}; } #[cfg(test)] macro_rules! negate { ( $rhs:expr ) => {{ crate::ast::Expression::UnaryOperator { op: UnaryOperator::Negate, expr: Box::new($rhs), span_op: Span::dummy(), } }}; } #[cfg(test)] macro_rules! factorial { ( $lhs:expr ) => {{ crate::ast::Expression::UnaryOperator { op: UnaryOperator::Factorial, expr: Box::new($lhs), span_op: Span::dummy(), } }}; } #[cfg(test)] macro_rules! binop { ( $lhs:expr, $op:ident, $rhs: expr ) => {{ crate::ast::Expression::BinaryOperator { op: BinaryOperator::$op, lhs: Box::new($lhs), rhs: Box::new($rhs), span_op: Some(Span::dummy()), } }}; } #[cfg(test)] macro_rules! conditional { ( $cond:expr, $lhs:expr, $rhs: expr ) => {{ crate::ast::Expression::Condition( Span::dummy(), Box::new($cond), Box::new($lhs), Box::new($rhs), ) }}; } #[cfg(test)] pub(crate) use binop; #[cfg(test)] pub(crate) use boolean; #[cfg(test)] pub(crate) use conditional; #[cfg(test)] pub(crate) use factorial; #[cfg(test)] pub(crate) use identifier; #[cfg(test)] pub(crate) use logical_neg; #[cfg(test)] pub(crate) use negate; #[cfg(test)] pub(crate) use scalar; #[derive(Debug, Clone, PartialEq)] pub enum TypeAnnotation { DimensionExpression(DimensionExpression), Bool(Span), String(Span), DateTime(Span), Fn(Span, Vec, Box), } impl TypeAnnotation { pub fn full_span(&self) -> Span { match self { TypeAnnotation::DimensionExpression(d) => d.full_span(), TypeAnnotation::Bool(span) => *span, TypeAnnotation::String(span) => *span, TypeAnnotation::DateTime(span) => *span, TypeAnnotation::Fn(span, _, _) => *span, } } } impl PrettyPrint for TypeAnnotation { fn pretty_print(&self) -> Markup { match self { TypeAnnotation::DimensionExpression(d) => d.pretty_print(), TypeAnnotation::Bool(_) => m::type_identifier("Bool"), TypeAnnotation::String(_) => m::type_identifier("String"), TypeAnnotation::DateTime(_) => m::type_identifier("DateTime"), TypeAnnotation::Fn(_, parameter_types, return_type) => { m::type_identifier("Fn") + m::operator("[(") + Itertools::intersperse( parameter_types.iter().map(|t| t.pretty_print()), m::operator(",") + m::space(), ) .sum() + m::operator(")") + m::space() + m::operator("->") + m::space() + return_type.pretty_print() + m::operator("]") } } } } #[derive(Debug, Clone, PartialEq)] pub enum DimensionExpression { Unity(Span), Dimension(Span, String), Multiply(Span, Box, Box), Divide(Span, Box, Box), Power( Option, // operator span, not available for unicode exponents Box, Span, // span for the exponent Exponent, ), } impl DimensionExpression { pub fn full_span(&self) -> Span { match self { DimensionExpression::Unity(s) => *s, DimensionExpression::Dimension(s, _) => *s, DimensionExpression::Multiply(span_op, lhs, rhs) => { span_op.extend(&lhs.full_span()).extend(&rhs.full_span()) } DimensionExpression::Divide(span_op, lhs, rhs) => { span_op.extend(&lhs.full_span()).extend(&rhs.full_span()) } DimensionExpression::Power(span_op, lhs, span_exponent, _exp) => match span_op { Some(span_op) => span_op.extend(&lhs.full_span()).extend(span_exponent), None => lhs.full_span().extend(span_exponent), }, } } } fn with_parens(dexpr: &DimensionExpression) -> Markup { match dexpr { expr @ (DimensionExpression::Unity(..) | DimensionExpression::Dimension(..) | DimensionExpression::Power(..)) => expr.pretty_print(), expr @ (DimensionExpression::Multiply(..) | DimensionExpression::Divide(..)) => { m::operator("(") + expr.pretty_print() + m::operator(")") } } } impl PrettyPrint for DimensionExpression { fn pretty_print(&self) -> Markup { match self { DimensionExpression::Unity(_) => m::type_identifier("1"), DimensionExpression::Dimension(_, ident) => m::type_identifier(ident), DimensionExpression::Multiply(_, lhs, rhs) => { lhs.pretty_print() + m::space() + m::operator("×") + m::space() + rhs.pretty_print() } DimensionExpression::Divide(_, lhs, rhs) => { lhs.pretty_print() + m::space() + m::operator("/") + m::space() + with_parens(rhs) } DimensionExpression::Power(_, lhs, _, exp) => { with_parens(lhs) + m::operator("^") + if exp.is_positive() { m::value(format!("{exp}")) } else { m::operator("(") + m::value(format!("{exp}")) + m::operator(")") } } } } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ProcedureKind { Print, Assert, AssertEq, Type, } #[derive(Debug, Clone, PartialEq)] pub enum Statement { Expression(Expression), DefineVariable { identifier_span: Span, identifier: String, expr: Expression, type_annotation: Option, decorators: Vec, }, DefineFunction { function_name_span: Span, function_name: String, type_parameters: Vec<(Span, String)>, /// Parameters, optionally with type annotations. The boolean argument specifies whether or not the parameter is variadic parameters: Vec<(Span, String, Option, bool)>, /// Function body. If it is absent, the function is implemented via FFI body: Option, return_type_annotation_span: Option, /// Optional annotated return type return_type_annotation: Option, }, DefineDimension(String, Vec), DefineBaseUnit(Span, String, Option, Vec), DefineDerivedUnit { identifier_span: Span, identifier: String, expr: Expression, type_annotation_span: Option, type_annotation: Option, decorators: Vec, }, ProcedureCall(Span, ProcedureKind, Vec), ModuleImport(Span, ModulePath), } #[cfg(test)] pub trait ReplaceSpans { fn replace_spans(&self) -> Self; } #[cfg(test)] impl ReplaceSpans for TypeAnnotation { fn replace_spans(&self) -> Self { match self { TypeAnnotation::DimensionExpression(d) => { TypeAnnotation::DimensionExpression(d.replace_spans()) } TypeAnnotation::Bool(_) => TypeAnnotation::Bool(Span::dummy()), TypeAnnotation::String(_) => TypeAnnotation::String(Span::dummy()), TypeAnnotation::DateTime(_) => TypeAnnotation::DateTime(Span::dummy()), TypeAnnotation::Fn(_, pt, rt) => { TypeAnnotation::Fn(Span::dummy(), pt.clone(), rt.clone()) } } } } #[cfg(test)] impl ReplaceSpans for DimensionExpression { fn replace_spans(&self) -> Self { match self { DimensionExpression::Unity(_) => DimensionExpression::Unity(Span::dummy()), DimensionExpression::Dimension(_, d) => { DimensionExpression::Dimension(Span::dummy(), d.clone()) } DimensionExpression::Multiply(_, lhs, rhs) => DimensionExpression::Multiply( Span::dummy(), Box::new(lhs.replace_spans()), Box::new(rhs.replace_spans()), ), DimensionExpression::Divide(_, lhs, rhs) => DimensionExpression::Divide( Span::dummy(), Box::new(lhs.replace_spans()), Box::new(rhs.replace_spans()), ), DimensionExpression::Power(span_op, lhs, _, exp) => DimensionExpression::Power( span_op.map(|_| Span::dummy()), Box::new(lhs.replace_spans()), Span::dummy(), *exp, ), } } } #[cfg(test)] impl ReplaceSpans for StringPart { fn replace_spans(&self) -> Self { match self { f @ StringPart::Fixed(_) => f.clone(), StringPart::Interpolation(_, expr) => { StringPart::Interpolation(Span::dummy(), Box::new(expr.replace_spans())) } } } } #[cfg(test)] impl ReplaceSpans for Expression { fn replace_spans(&self) -> Self { match self { Expression::Scalar(_, name) => Expression::Scalar(Span::dummy(), *name), Expression::Identifier(_, name) => Expression::Identifier(Span::dummy(), name.clone()), Expression::UnitIdentifier(_, prefix, name, full_name) => { Expression::UnitIdentifier(Span::dummy(), *prefix, name.clone(), full_name.clone()) } Expression::UnaryOperator { op, expr, span_op: _, } => Expression::UnaryOperator { op: *op, expr: Box::new(expr.replace_spans()), span_op: Span::dummy(), }, Expression::BinaryOperator { op, lhs, rhs, span_op: _, } => Expression::BinaryOperator { op: *op, lhs: Box::new(lhs.replace_spans()), rhs: Box::new(rhs.replace_spans()), span_op: Some(Span::dummy()), }, Expression::FunctionCall(_, _, callable, args) => Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(callable.replace_spans()), args.iter().map(|a| a.replace_spans()).collect(), ), Expression::Boolean(_, val) => Expression::Boolean(Span::dummy(), *val), Expression::Condition(_, condition, then, else_) => Expression::Condition( Span::dummy(), Box::new(condition.replace_spans()), Box::new(then.replace_spans()), Box::new(else_.replace_spans()), ), Expression::String(_, parts) => Expression::String( Span::dummy(), parts.iter().map(|p| p.replace_spans()).collect(), ), } } } #[cfg(test)] impl ReplaceSpans for Statement { fn replace_spans(&self) -> Self { match self { Statement::Expression(expr) => Statement::Expression(expr.replace_spans()), Statement::DefineVariable { identifier_span: _, identifier, expr, type_annotation, decorators, } => Statement::DefineVariable { identifier_span: Span::dummy(), identifier: identifier.clone(), expr: expr.replace_spans(), type_annotation: type_annotation.as_ref().map(|t| t.replace_spans()), decorators: decorators.clone(), }, Statement::DefineFunction { function_name_span: _, function_name, type_parameters, parameters, body, return_type_annotation_span: return_type_span, return_type_annotation, } => Statement::DefineFunction { function_name_span: Span::dummy(), function_name: function_name.clone(), type_parameters: type_parameters .iter() .map(|(_, name)| (Span::dummy(), name.clone())) .collect(), parameters: parameters .iter() .map(|(_, name, type_, is_variadic)| { ( Span::dummy(), name.clone(), type_.as_ref().map(|t| t.replace_spans()), *is_variadic, ) }) .collect(), body: body.clone().map(|b| b.replace_spans()), return_type_annotation_span: return_type_span.map(|_| Span::dummy()), return_type_annotation: return_type_annotation.as_ref().map(|t| t.replace_spans()), }, Statement::DefineDimension(name, dexprs) => Statement::DefineDimension( name.clone(), dexprs.iter().map(|t| t.replace_spans()).collect(), ), Statement::DefineBaseUnit(_, name, type_, decorators) => Statement::DefineBaseUnit( Span::dummy(), name.clone(), type_.as_ref().map(|t| t.replace_spans()), decorators.clone(), ), Statement::DefineDerivedUnit { identifier_span: _, identifier, expr, type_annotation_span, type_annotation, decorators, } => Statement::DefineDerivedUnit { identifier_span: Span::dummy(), identifier: identifier.clone(), expr: expr.replace_spans(), type_annotation_span: type_annotation_span.map(|_| Span::dummy()), type_annotation: type_annotation.as_ref().map(|t| t.replace_spans()), decorators: decorators.clone(), }, Statement::ProcedureCall(_, proc, args) => Statement::ProcedureCall( Span::dummy(), proc.clone(), args.iter().map(|a| a.replace_spans()).collect(), ), Statement::ModuleImport(_, module_path) => { Statement::ModuleImport(Span::dummy(), module_path.clone()) } } } } #[cfg(test)] impl ReplaceSpans for Vec { fn replace_spans(&self) -> Self { self.iter().map(|s| s.replace_spans()).collect() } } numbat-1.11.0/src/buffered_writer.rs000064400000000000000000000001501046102023000155010ustar 00000000000000use termcolor::WriteColor; pub trait BufferedWriter: WriteColor { fn to_string(&self) -> String; } numbat-1.11.0/src/bytecode_interpreter.rs000064400000000000000000000477201046102023000165620ustar 00000000000000use std::collections::HashMap; use crate::ast::ProcedureKind; use crate::decorator::Decorator; use crate::interpreter::{ Interpreter, InterpreterResult, InterpreterSettings, Result, RuntimeError, }; use crate::name_resolution::LAST_RESULT_IDENTIFIERS; use crate::prefix::Prefix; use crate::prefix_parser::AcceptsPrefix; use crate::pretty_print::PrettyPrint; use crate::typed_ast::{BinaryOperator, Expression, Statement, StringPart, UnaryOperator}; use crate::unit::{CanonicalName, Unit}; use crate::unit_registry::{UnitMetadata, UnitRegistry}; use crate::value::FunctionReference; use crate::vm::{Constant, ExecutionContext, Op, Vm}; use crate::{decorator, ffi}; #[derive(Debug, Clone, Default)] pub struct LocalMetadata { pub name: Option, pub url: Option, pub aliases: Vec, } #[derive(Debug, Clone)] pub struct Local { identifier: String, depth: usize, pub metadata: LocalMetadata, } #[derive(Clone)] pub struct BytecodeInterpreter { vm: Vm, /// List of local variables currently in scope, one vector for each scope (for now: 0: 'global' scope, 1: function scope) locals: Vec>, // Maps names of units to indices of the respective constants in the VM unit_name_to_constant_index: HashMap, /// List of functions functions: HashMap, } impl BytecodeInterpreter { fn compile_expression(&mut self, expr: &Expression) -> Result<()> { match expr { Expression::Scalar(_span, n) => { let index = self.vm.add_constant(Constant::Scalar(n.to_f64())); self.vm.add_op1(Op::LoadConstant, index); } Expression::Identifier(_span, identifier, _type) => { // Searching in reverse order ensures that we find the innermost identifier of that name first (shadowing) let current_depth = self.locals.len() - 1; if let Some(position) = self.locals[current_depth] .iter() .rposition(|l| &l.identifier == identifier && l.depth == current_depth) { self.vm.add_op1(Op::GetLocal, position as u16); // TODO: check overflow } else if let Some(upvalue_position) = self.locals[0] .iter() .rposition(|l| &l.identifier == identifier) { self.vm.add_op1(Op::GetUpvalue, upvalue_position as u16); } else if LAST_RESULT_IDENTIFIERS.contains(&identifier.as_str()) { self.vm.add_op(Op::GetLastResult); } else if let Some(is_foreign) = self.functions.get(identifier) { let index = self .vm .add_constant(Constant::FunctionReference(if *is_foreign { FunctionReference::Foreign(identifier.clone()) } else { FunctionReference::Normal(identifier.clone()) })); self.vm.add_op1(Op::LoadConstant, index); } else { unreachable!("Unknown identifier '{identifier}'") } } Expression::UnitIdentifier(_span, prefix, unit_name, _full_name, _type) => { let index = self .unit_name_to_constant_index .get(unit_name) .expect("unit should already exist"); self.vm.add_op1(Op::LoadConstant, *index); if prefix != &Prefix::none() { let prefix_idx = self.vm.add_prefix(*prefix); self.vm.add_op1(Op::ApplyPrefix, prefix_idx); } } Expression::UnaryOperator(_span, UnaryOperator::Negate, rhs, _type) => { self.compile_expression(rhs)?; self.vm.add_op(Op::Negate); } Expression::UnaryOperator(_span, UnaryOperator::Factorial, lhs, _type) => { self.compile_expression(lhs)?; self.vm.add_op(Op::Factorial); } Expression::UnaryOperator(_span, UnaryOperator::LogicalNeg, lhs, _type) => { self.compile_expression(lhs)?; self.vm.add_op(Op::LogicalNeg); } Expression::BinaryOperator(_span, operator, lhs, rhs, _type) => { self.compile_expression(lhs)?; self.compile_expression(rhs)?; let op = match operator { BinaryOperator::Add => Op::Add, BinaryOperator::Sub => Op::Subtract, BinaryOperator::Mul => Op::Multiply, BinaryOperator::Div => Op::Divide, BinaryOperator::Power => Op::Power, BinaryOperator::ConvertTo => Op::ConvertTo, BinaryOperator::LessThan => Op::LessThan, BinaryOperator::GreaterThan => Op::GreaterThan, BinaryOperator::LessOrEqual => Op::LessOrEqual, BinaryOperator::GreaterOrEqual => Op::GreatorOrEqual, BinaryOperator::Equal => Op::Equal, BinaryOperator::NotEqual => Op::NotEqual, BinaryOperator::LogicalAnd => Op::LogicalAnd, BinaryOperator::LogicalOr => Op::LogicalOr, }; self.vm.add_op(op); } Expression::BinaryOperatorForDate(_span, operator, lhs, rhs, type_) => { self.compile_expression(lhs)?; self.compile_expression(rhs)?; // if the result is a duration: let op = if type_.is_dtype() { // the VM will need to return a value with the units of Seconds. so look up that unit here, and push it // onto the stack, so the VM can easily reference it. // TODO: We do not want to hard-code 'second' here. Instead, we might // introduce a decorator to register the 'second' unit in the prelude for // this specific purpose. We also need to handle errors in case no such unit // was registered. let second_idx = self.unit_name_to_constant_index.get("second"); self.vm.add_op1(Op::LoadConstant, *second_idx.unwrap()); Op::DiffDateTime } else { match operator { BinaryOperator::Add => Op::AddToDateTime, BinaryOperator::Sub => Op::SubFromDateTime, _ => unreachable!("{operator:?} is not valid with a DateTime"), // should be unreachable, because the typechecker will error first } }; self.vm.add_op(op); } Expression::FunctionCall(_span, _full_span, name, args, _type) => { // Put all arguments on top of the stack for arg in args { self.compile_expression_with_simplify(arg)?; } if let Some(idx) = self.vm.get_ffi_callable_idx(name) { // TODO: check overflow: self.vm.add_op2(Op::FFICallFunction, idx, args.len() as u16); } else { let idx = self.vm.get_function_idx(name); self.vm.add_op2(Op::Call, idx, args.len() as u16); // TODO: check overflow } } Expression::CallableCall(_span, callable, args, _type) => { // Put all arguments on top of the stack for arg in args { self.compile_expression_with_simplify(arg)?; } // Put the callable on top of the stack self.compile_expression(callable)?; self.vm.add_op1(Op::CallCallable, args.len() as u16); } Expression::Boolean(_, val) => { let index = self.vm.add_constant(Constant::Boolean(*val)); self.vm.add_op1(Op::LoadConstant, index); } Expression::String(_, string_parts) => { for part in string_parts { match part { StringPart::Fixed(s) => { let index = self.vm.add_constant(Constant::String(s.clone())); self.vm.add_op1(Op::LoadConstant, index) } StringPart::Interpolation(_, expr) => { self.compile_expression_with_simplify(expr)?; } } } self.vm.add_op1(Op::JoinString, string_parts.len() as u16); // TODO: this can overflow } Expression::Condition(_, condition, then_expr, else_expr) => { self.compile_expression(condition)?; let if_jump_offset = self.vm.current_offset() + 1; // +1 for the opcode self.vm.add_op1(Op::JumpIfFalse, 0xffff); self.compile_expression(then_expr)?; let else_jump_offset = self.vm.current_offset() + 1; self.vm.add_op1(Op::Jump, 0xffff); let else_block_offset = self.vm.current_offset(); self.vm .patch_u16_value_at(if_jump_offset, else_block_offset - (if_jump_offset + 2)); self.compile_expression(else_expr)?; let end_offset = self.vm.current_offset(); self.vm .patch_u16_value_at(else_jump_offset, end_offset - (else_jump_offset + 2)); } }; Ok(()) } fn compile_expression_with_simplify(&mut self, expr: &Expression) -> Result<()> { self.compile_expression(expr)?; match expr { Expression::Scalar(..) | Expression::Identifier(..) | Expression::UnitIdentifier(..) | Expression::FunctionCall(..) | Expression::CallableCall(..) | Expression::UnaryOperator(..) | Expression::BinaryOperator(_, BinaryOperator::ConvertTo, _, _, _) | Expression::Boolean(..) | Expression::String(..) | Expression::Condition(..) => {} Expression::BinaryOperator(..) | Expression::BinaryOperatorForDate(..) => { self.vm.add_op(Op::FullSimplify); } } Ok(()) } fn compile_statement(&mut self, stmt: &Statement) -> Result<()> { match stmt { Statement::Expression(expr) => { self.compile_expression_with_simplify(expr)?; self.vm.add_op(Op::Return); } Statement::DefineVariable(identifier, decorators, expr, _type_annotation, _type) => { let current_depth = self.current_depth(); // For variables, we ignore the prefix info and only use the names let aliases = crate::decorator::name_and_aliases(identifier, decorators) .map(|(name, _)| name) .cloned() .collect::>(); let metadata = LocalMetadata { name: crate::decorator::name(decorators), url: crate::decorator::url(decorators), aliases: aliases.clone(), }; for alias_name in aliases { self.compile_expression_with_simplify(expr)?; self.locals[current_depth].push(Local { identifier: alias_name.clone(), depth: 0, metadata: metadata.clone(), }); } } Statement::DefineFunction( name, _type_parameters, parameters, Some(expr), _return_type_annotation, _return_type, ) => { self.vm.begin_function(name); self.locals.push(vec![]); let current_depth = self.current_depth(); for parameter in parameters { self.locals[current_depth].push(Local { identifier: parameter.1.clone(), depth: current_depth, metadata: LocalMetadata::default(), }); } self.compile_expression_with_simplify(expr)?; self.vm.add_op(Op::Return); self.locals.pop(); self.vm.end_function(); self.functions.insert(name.clone(), false); } Statement::DefineFunction( name, _type_parameters, parameters, None, _return_type_annotation, _return_type, ) => { // Declaring a foreign function does not generate any bytecode. But we register // its name and arity here to be able to distinguish it from normal functions. let is_variadic = parameters.iter().any(|p| p.2); self.vm.add_foreign_function( name, if is_variadic { 1..=usize::MAX } else { parameters.len()..=parameters.len() }, ); self.functions.insert(name.clone(), true); } Statement::DefineDimension(_name, _dexprs) => { // Declaring a dimension is like introducing a new type. The information // is only relevant for the type checker. Nothing happens at run time. } Statement::DefineBaseUnit(unit_name, decorators, readable_type, type_) => { let aliases = decorator::name_and_aliases(unit_name, decorators) .map(|(name, ap)| (name.clone(), ap)) .collect(); self.vm .unit_registry .add_base_unit( unit_name, UnitMetadata { type_: type_.clone(), readable_type: readable_type.clone(), aliases, name: decorator::name(decorators), canonical_name: decorator::get_canonical_unit_name( unit_name, decorators, ), url: decorator::url(decorators), binary_prefixes: decorators.contains(&Decorator::BinaryPrefixes), metric_prefixes: decorators.contains(&Decorator::MetricPrefixes), }, ) .map_err(RuntimeError::UnitRegistryError)?; let constant_idx = self.vm.add_constant(Constant::Unit(Unit::new_base( unit_name, crate::decorator::get_canonical_unit_name(unit_name.as_str(), &decorators[..]), ))); for (name, _) in decorator::name_and_aliases(unit_name, decorators) { self.unit_name_to_constant_index .insert(name.into(), constant_idx); } } Statement::DefineDerivedUnit(unit_name, expr, decorators, readable_type, type_) => { let aliases = decorator::name_and_aliases(unit_name, decorators) .map(|(name, ap)| (name.clone(), ap)) .collect(); let constant_idx = self.vm.add_constant(Constant::Unit(Unit::new_base( "", CanonicalName { name: "".to_string(), accepts_prefix: AcceptsPrefix::both(), }, ))); // TODO: dummy is just a temp. value until the SetUnitConstant op runs let unit_information_idx = self.vm.add_unit_information( unit_name, Some( &crate::decorator::get_canonical_unit_name( unit_name.as_str(), &decorators[..], ) .name, ), UnitMetadata { type_: type_.clone(), readable_type: readable_type.clone(), aliases, name: decorator::name(decorators), canonical_name: decorator::get_canonical_unit_name(unit_name, decorators), url: decorator::url(decorators), binary_prefixes: decorators.contains(&Decorator::BinaryPrefixes), metric_prefixes: decorators.contains(&Decorator::MetricPrefixes), }, ); // TODO: there is some asymmetry here because we do not introduce identifiers for base units self.compile_expression_with_simplify(expr)?; self.vm .add_op2(Op::SetUnitConstant, unit_information_idx, constant_idx); // TODO: code duplication with DeclareBaseUnit branch above for (name, _) in decorator::name_and_aliases(unit_name, decorators) { self.unit_name_to_constant_index .insert(name.into(), constant_idx); } } Statement::ProcedureCall(ProcedureKind::Type, args) => { assert_eq!(args.len(), 1); let arg = &args[0]; use crate::markup as m; let idx = self.vm.add_string( m::dimmed("=") + m::whitespace(" ") + arg.get_type().pretty_print(), ); self.vm.add_op1(Op::PrintString, idx); } Statement::ProcedureCall(kind, args) => { // Put all arguments on top of the stack for arg in args { self.compile_expression_with_simplify(arg)?; } let name = &ffi::procedures().get(kind).unwrap().name; let idx = self.vm.get_ffi_callable_idx(name).unwrap(); self.vm .add_op2(Op::FFICallProcedure, idx, args.len() as u16); // TODO: check overflow } } Ok(()) } fn run(&mut self, settings: &mut InterpreterSettings) -> Result { let mut ctx = ExecutionContext { print_fn: &mut settings.print_fn, }; self.vm.disassemble(); let result = self.vm.run(&mut ctx); self.vm.debug(); result } pub(crate) fn set_debug(&mut self, activate: bool) { self.vm.set_debug(activate); } fn current_depth(&self) -> usize { self.locals.len() - 1 } pub fn get_defining_unit(&self, unit_name: &str) -> Option<&Unit> { self.unit_name_to_constant_index .get(unit_name) .and_then(|idx| self.vm.constants.get(*idx as usize)) .and_then(|constant| match constant { Constant::Unit(u) => Some(u), _ => None, }) } pub fn lookup_global(&self, name: &str) -> Option<&Local> { self.locals[0].iter().find(|l| l.identifier == name) } } impl Interpreter for BytecodeInterpreter { fn new() -> Self { Self { vm: Vm::new(), locals: vec![vec![]], unit_name_to_constant_index: HashMap::new(), functions: HashMap::new(), } } fn interpret_statements( &mut self, settings: &mut InterpreterSettings, statements: &[Statement], ) -> Result { for statement in statements { self.compile_statement(statement)?; } self.run(settings) } fn get_unit_registry(&self) -> &UnitRegistry { &self.vm.unit_registry } } numbat-1.11.0/src/column_formatter.rs000064400000000000000000000114371046102023000157150ustar 00000000000000use unicode_width::UnicodeWidthStr; use crate::markup as m; use crate::markup::{FormatType, FormattedString, Markup, OutputType}; /// Do not show tables wider than this for readabilty reasons const MAX_WIDTH: usize = 160; pub struct ColumnFormatter { terminal_width: usize, padding: usize, } impl ColumnFormatter { pub fn new(terminal_width: usize) -> Self { Self { terminal_width: terminal_width.min(MAX_WIDTH), padding: 2, } } pub fn format>( &self, entries: impl IntoIterator, format: FormatType, ) -> Markup { let mut result = m::empty(); let entries: Vec<_> = entries .into_iter() .map(|s| s.as_ref().to_string()) .collect(); if let Some(max_entry_width) = entries.iter().map(|s| s.width()).max() { let column_width = max_entry_width + self.padding; let min_num_columns = self.terminal_width / column_width; if min_num_columns < 1 { for entry in entries { result += Markup::from(FormattedString(OutputType::Normal, format, entry)) + m::whitespace(" ".repeat(self.padding)); } return result; } for num_columns in min_num_columns..=self.terminal_width { // TODO: once we have Rust 1.73, use the div_ceil implementation: // let num_rows = entries.len().div_ceil(num_columns); let num_rows = (entries.len() + num_columns - 1) / num_columns; let mut table: Vec>> = vec![vec![None; num_columns]; num_rows]; for (idx, entry) in entries.iter().enumerate() { let row = idx % num_rows; let col = idx / num_rows; table[row][col] = Some(entry); } let column_widths: Vec = (0..num_columns) .map(|c| { (0..num_rows) .map(|r| table[r][c].map(|e| e.width()).unwrap_or(0)) .max() .unwrap_or(0) + self.padding }) .collect(); if column_widths.iter().sum::() > self.terminal_width { // Return result from previous iteration return result; } result = m::empty(); for row in table { for (col, entry) in row.iter().enumerate() { if let Some(entry) = entry { let width = entry.width(); let whitespace_length = column_widths[col] - width; result += Markup::from(FormattedString( OutputType::Normal, format, (*entry).into(), )); result += m::whitespace(" ".repeat(whitespace_length)); } else { break; } } result += m::nl(); } } result } else { result } } } #[cfg(test)] fn format(width: usize, entries: &[&str]) -> String { use crate::markup::{Formatter, PlainTextFormatter}; let formatter = ColumnFormatter::new(width); PlainTextFormatter {}.format(&formatter.format(entries, FormatType::Text), false) } #[test] fn test_column_formatter() { use insta::assert_snapshot; { let elements = &["one", "two", "three", "four", "five", "six", "seven"]; assert_snapshot!(format(80, elements), @r###" one two three four five six seven "###); assert_snapshot!(format(42, elements), @r###" one two three four five six seven "###); assert_snapshot!(format(21, elements), @r###" one four seven two five three six "###); assert_snapshot!(format(20, elements), @r###" one four seven two five three six "###); assert_snapshot!(format(10, elements), @r###" one two three four five six seven "###); assert_snapshot!(format(4, elements), @"one two three four five six seven "); } assert_snapshot!(format(80, &["one"]), @"one"); assert_snapshot!(format(80, &[]), @""); assert_snapshot!(format(12, &["aaaa", "bbbb", "cccc", "dddd"]), @r###" aaaa cccc bbbb dddd "###); } numbat-1.11.0/src/currency.rs000064400000000000000000000020701046102023000141600ustar 00000000000000use std::sync::{Mutex, MutexGuard, OnceLock}; use numbat_exchange_rates::{parse_exchange_rates, ExchangeRates}; static EXCHANGE_RATES: OnceLock>> = OnceLock::new(); pub struct ExchangeRatesCache {} impl ExchangeRatesCache { pub fn new() -> Self { Self {} } pub fn get_rate(&self, currency: &str) -> Option { let rates = Self::fetch(); rates.as_ref().and_then(|r| r.get(currency)).cloned() } pub fn set_from_xml(xml_content: &str) { EXCHANGE_RATES .set(Mutex::new(parse_exchange_rates(xml_content))) .unwrap(); } #[cfg(feature = "fetch-exchangerates")] pub fn fetch() -> MutexGuard<'static, Option> { EXCHANGE_RATES .get_or_init(|| Mutex::new(numbat_exchange_rates::fetch_exchange_rates())) .lock() .unwrap() } #[cfg(not(feature = "fetch-exchangerates"))] pub fn fetch() -> MutexGuard<'static, Option> { EXCHANGE_RATES.get().unwrap().lock().unwrap() } } numbat-1.11.0/src/datetime.rs000064400000000000000000000053341046102023000141300ustar 00000000000000use chrono::{DateTime, Datelike, FixedOffset, LocalResult}; use chrono_tz::Tz; pub fn get_local_timezone() -> Option { let tz_str = iana_time_zone::get_timezone().ok()?; tz_str.parse().ok() } pub fn get_local_timezone_or_utc() -> Tz { get_local_timezone().unwrap_or(chrono_tz::UTC) } pub fn parse_datetime(input: &str) -> Option> { if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(input) { Some(dt) } else if let Ok(dt) = chrono::DateTime::parse_from_rfc2822(input) { Some(dt) } else { const FORMATS: [&str; 8] = [ // 24 hour formats: "%Y-%m-%d %H:%M:%S%.f", "%Y/%m/%d %H:%M:%S%.f", "%Y-%m-%d %H:%M", "%Y/%m/%d %H:%M", // 12 hour formats: "%Y-%m-%d %I:%M:%S%.f %p", "%Y-%m-%d %I:%M %p", "%Y/%m/%d %I:%M:%S%.f %p", "%Y/%m/%d %I:%M %p", ]; for format in FORMATS { // Try to match the given format plus an additional UTC offset (%z) if let Ok(dt) = chrono::DateTime::parse_from_str(input, &format!("{format} %z")) { return Some(dt); } // Try to match the given format plus an additional timezone name (%Z). // chrono does not support %Z, so we implement this ourselves. We were // warned by developers before us not to write timezone-related code on // our own, so we're probably going to regret this. // Get the last space-separated word in the input string, and try to parse it // as a timezone specifier, then try to match the rest of the string with the // given format. if let Some((rest, potential_timezone_name)) = input.rsplit_once(' ') { if let Ok(tz) = potential_timezone_name.parse::() { if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(rest, format) { if let LocalResult::Single(dt) = ndt.and_local_timezone(tz) { return Some(dt.fixed_offset()); } } } } // Without timezone/offset if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(input, format) { if let LocalResult::Single(dt) = ndt.and_local_timezone(get_local_timezone_or_utc()) { return Some(dt.fixed_offset()); } } } None } } pub fn to_rfc2822_save(dt: &DateTime) -> String { if dt.year() < 0 || dt.year() > 9999 { "".to_string() } else { dt.to_rfc2822() } } numbat-1.11.0/src/decorator.rs000064400000000000000000000045641046102023000143220ustar 00000000000000use crate::{prefix_parser::AcceptsPrefix, unit::CanonicalName}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Decorator { MetricPrefixes, BinaryPrefixes, Aliases(Vec<(String, Option)>), Url(String), Name(String), } pub fn name_and_aliases<'a>( name: &'a String, decorators: &'a [Decorator], ) -> Box + 'a> { let aliases = { let mut aliases_vec = vec![]; for decorator in decorators { if let Decorator::Aliases(aliases) = decorator { aliases_vec = aliases .iter() .map(|(name, accepts_prefix)| { (name, accepts_prefix.unwrap_or(AcceptsPrefix::only_long())) }) .collect(); } } aliases_vec }; if !aliases.iter().any(|(n, _)| n == &name) { let name_iter = std::iter::once((name, AcceptsPrefix::only_long())); Box::new(name_iter.chain(aliases)) } else { Box::new(aliases.into_iter()) } } pub fn get_canonical_unit_name(unit_name: &str, decorators: &[Decorator]) -> CanonicalName { for decorator in decorators { if let Decorator::Aliases(aliases) = decorator { for (alias, accepts_prefix) in aliases { match accepts_prefix { &Some(ap) if ap.short => { return CanonicalName::new(alias, ap); } _ => {} } } } } CanonicalName { name: unit_name.into(), accepts_prefix: AcceptsPrefix::only_long(), } } pub fn name(decorators: &[Decorator]) -> Option { for decorator in decorators { if let Decorator::Name(name) = decorator { return Some(name.clone()); } } None } pub fn url(decorators: &[Decorator]) -> Option { for decorator in decorators { if let Decorator::Url(url) = decorator { return Some(url.clone()); } } None } pub fn contains_aliases_with_prefixes(decorates: &[Decorator]) -> bool { for decorator in decorates { if let Decorator::Aliases(aliases) = decorator { if aliases.iter().any(|(_, prefixes)| prefixes.is_some()) { return true; } } } false } numbat-1.11.0/src/diagnostic.rs000064400000000000000000000407171046102023000144640ustar 00000000000000use codespan_reporting::diagnostic::LabelStyle; use crate::{ interpreter::RuntimeError, parser::ParseError, pretty_print::PrettyPrint, resolver::ResolverError, typechecker::{IncompatibleDimensionsError, TypeCheckError}, NameResolutionError, }; pub type Diagnostic = codespan_reporting::diagnostic::Diagnostic; pub trait ErrorDiagnostic { fn diagnostics(&self) -> Vec; } impl ErrorDiagnostic for ParseError { fn diagnostics(&self) -> Vec { vec![Diagnostic::error() .with_message("while parsing") .with_labels(vec![self .span .diagnostic_label(LabelStyle::Primary) .with_message(self.kind.to_string())])] } } impl ErrorDiagnostic for ResolverError { fn diagnostics(&self) -> Vec { match self { ResolverError::UnknownModule(span, _) => vec![Diagnostic::error() .with_message("while resolving imports in") .with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message("Unknown module")])], ResolverError::ParseErrors(errors) => { errors.iter().flat_map(|e| e.diagnostics()).collect() } } } } impl ErrorDiagnostic for NameResolutionError { fn diagnostics(&self) -> Vec { match self { NameResolutionError::IdentifierClash { conflicting_identifier: _, conflict_span, original_span, } => vec![Diagnostic::error() .with_message("identifier clash in definition") .with_labels(vec![ original_span .diagnostic_label(LabelStyle::Secondary) .with_message("Previously defined here"), conflict_span .diagnostic_label(LabelStyle::Primary) .with_message("identifier is already in use"), ])], NameResolutionError::ReservedIdentifier(span) => vec![Diagnostic::error() .with_message("reserved identifier may not be used") .with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message("reserved identifier")])], } } } impl ErrorDiagnostic for TypeCheckError { fn diagnostics(&self) -> Vec { let d = Diagnostic::error().with_message("while type checking"); let inner_error = format!("{}", self); let d = match self { TypeCheckError::UnknownIdentifier(span, _, suggestion) => { let notes = if let Some(suggestion) = suggestion { vec![format!("Did you mean '{suggestion}'?")] } else { vec![] }; d.with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message("unknown identifier")]) .with_notes(notes) } TypeCheckError::IncompatibleDimensions(IncompatibleDimensionsError { operation, span_operation, span_actual, actual_type, actual_dimensions, span_expected, expected_type, expected_dimensions, .. }) => { let expected_type = if expected_dimensions.is_empty() { format!("{expected_type}") } else { expected_dimensions.join(" or ") }; let actual_type = if actual_dimensions.is_empty() { format!("{actual_type}") } else { actual_dimensions.join(" or ") }; let labels = vec![ span_expected .diagnostic_label(LabelStyle::Primary) .with_message(expected_type), span_actual .diagnostic_label(LabelStyle::Primary) .with_message(actual_type), span_operation .diagnostic_label(LabelStyle::Secondary) .with_message(format!("incompatible dimensions in {}", operation)), ]; d.with_labels(labels).with_notes(vec![inner_error]) } TypeCheckError::NonScalarExponent(span, type_) | TypeCheckError::NonScalarFactorialArgument(span, type_) => d .with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(format!("{type_}"))]) .with_notes(vec![inner_error]), TypeCheckError::UnsupportedConstEvalExpression(span, _) => d.with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(inner_error)]), TypeCheckError::DivisionByZeroInConstEvalExpression(span) => d.with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(inner_error)]), TypeCheckError::RegistryError(re) => match re { crate::registry::RegistryError::EntryExists(_) => d.with_notes(vec![inner_error]), crate::registry::RegistryError::UnknownEntry(name, suggestion) => { d.with_notes(vec![format!( "Unknown dimension '{name}'{maybe_suggestion}", maybe_suggestion = if let Some(suggestion) = suggestion { format!(" did you mean '{suggestion}'?") } else { "".into() } )]) } }, TypeCheckError::IncompatibleAlternativeDimensionExpression( _name, span1, type1, span2, type2, ) => d .with_labels(vec![ span1 .diagnostic_label(LabelStyle::Primary) .with_message(type1.to_string()), span2 .diagnostic_label(LabelStyle::Primary) .with_message(type2.to_string()), ]) .with_notes(vec![inner_error]), TypeCheckError::WrongArity { callable_span, callable_name: _, callable_definition_span, arity, num_args, } => { let mut labels = vec![callable_span .diagnostic_label(LabelStyle::Primary) .with_message(format!( "{what}was called with {num}, but takes {range}", what = if callable_definition_span.is_some() { "" } else { "procedure or function object " }, num = if *num_args == 1 { "one argument".into() } else { format!("{num_args} arguments") }, range = if arity.start() == arity.end() { format!("{}", arity.start()) } else { format!("{} to {}", arity.start(), arity.end()) } ))]; if let Some(span) = callable_definition_span { labels.insert( 0, span.diagnostic_label(LabelStyle::Secondary) .with_message("The function defined here"), ); } d.with_labels(labels) } TypeCheckError::TypeParameterNameClash(span, _) => d.with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(inner_error)]), TypeCheckError::CanNotInferTypeParameters( span, callable_definition_span, _, params, ) => d.with_labels(vec![ callable_definition_span .diagnostic_label(LabelStyle::Secondary) .with_message(format!( "The type parameter(s) {params} in this generic function" )), span.diagnostic_label(LabelStyle::Primary) .with_message("… could not be inferred for this function call"), ]), TypeCheckError::MultipleUnresolvedTypeParameters(span, parameter_span) => d .with_labels(vec![ span.diagnostic_label(LabelStyle::Secondary) .with_message("In this function call"), parameter_span .diagnostic_label(LabelStyle::Primary) .with_message(inner_error), ]), TypeCheckError::IncompatibleTypesInCondition( if_span, then_type, then_span, else_type, else_span, ) => d.with_labels(vec![ then_span .diagnostic_label(LabelStyle::Secondary) .with_message(then_type.to_string()), else_span .diagnostic_label(LabelStyle::Secondary) .with_message(else_type.to_string()), if_span.diagnostic_label(LabelStyle::Primary).with_message( "Incompatible types in 'then' and 'else' branches of conditional", ), ]), TypeCheckError::IncompatibleTypesInComparison( op_span, lhs_type, lhs_span, rhs_type, rhs_span, ) => d.with_labels(vec![ lhs_span .diagnostic_label(LabelStyle::Secondary) .with_message(lhs_type.to_string()), rhs_span .diagnostic_label(LabelStyle::Secondary) .with_message(rhs_type.to_string()), op_span .diagnostic_label(LabelStyle::Primary) .with_message("Incompatible types comparison operator"), ]), TypeCheckError::IncompatibleTypeInAssert(procedure_span, type_, type_span) => d .with_labels(vec![ type_span .diagnostic_label(LabelStyle::Secondary) .with_message(type_.to_string()), procedure_span .diagnostic_label(LabelStyle::Primary) .with_message("Non-boolean type in 'assert' call"), ]), TypeCheckError::IncompatibleTypesInAssertEq( procedure_span, first_type, first_span, arg_type, arg_span, ) => d.with_labels(vec![ first_span .diagnostic_label(LabelStyle::Secondary) .with_message(first_type.to_string()), arg_span .diagnostic_label(LabelStyle::Secondary) .with_message(arg_type.to_string()), procedure_span .diagnostic_label(LabelStyle::Primary) .with_message("Incompatible types in 'assert_eq' call"), ]), TypeCheckError::IncompatibleTypesInAnnotation( what, what_span, annotation, annotation_span, deduced_type, body_span, ) => d.with_labels(vec![ annotation_span .diagnostic_label(LabelStyle::Secondary) .with_message(annotation.to_string()), body_span .diagnostic_label(LabelStyle::Secondary) .with_message(deduced_type.to_string()), what_span .diagnostic_label(LabelStyle::Primary) .with_message(format!("Incompatible types in {what}")), ]), TypeCheckError::IncompatibleTypesInFunctionCall( parameter_span, parameter_type, argument_span, argument_type, ) => { if let Some(parameter_span) = parameter_span { d.with_labels(vec![ parameter_span .diagnostic_label(LabelStyle::Secondary) .with_message(parameter_type.to_string()), argument_span .diagnostic_label(LabelStyle::Primary) .with_message(argument_type.to_string()), ]) .with_notes(vec![inner_error]) } else { d.with_labels(vec![argument_span .diagnostic_label(LabelStyle::Primary) .with_message(argument_type.to_string())]) .with_notes(vec![inner_error]) } } TypeCheckError::NameAlreadyUsedBy(_, definition_span, previous_definition_span) => { let mut labels = vec![]; if let Some(span) = previous_definition_span { labels.push( span.diagnostic_label(LabelStyle::Secondary) .with_message("Previously defined here"), ); } labels.push( definition_span .diagnostic_label(LabelStyle::Primary) .with_message(inner_error), ); d.with_labels(labels) } TypeCheckError::NoDimensionlessBaseUnit(span, unit_name) => d .with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(inner_error)]) .with_notes(vec![ format!("Use 'unit {unit_name}' for ad-hoc units."), format!("Use 'unit {unit_name}: Scalar = …' for derived units."), ]), TypeCheckError::ForeignFunctionNeedsTypeAnnotations(span, _) | TypeCheckError::UnknownForeignFunction(span, _) | TypeCheckError::NonRationalExponent(span) | TypeCheckError::OverflowInConstExpr(span) | TypeCheckError::ExpectedDimensionType(span, _) | TypeCheckError::ExpectedBool(span) | TypeCheckError::NoFunctionReferenceToGenericFunction(span) | TypeCheckError::OnlyFunctionsAndReferencesCanBeCalled(span) => d .with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(inner_error)]), TypeCheckError::MissingDimension(span, dim) => d .with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(format!("Missing dimension '{dim}'"))]) .with_notes(vec![format!( "This operation requires the '{dim}' dimension to be defined" )]), TypeCheckError::IncompatibleTypesInOperator( span, op, lhs_type, lhs_span, rhs_type, rhs_span, ) => d.with_labels(vec![ span.diagnostic_label(LabelStyle::Primary) .with_message(format!( "Operator {} can not be applied to these types", op.pretty_print() )), lhs_span .diagnostic_label(LabelStyle::Secondary) .with_message(lhs_type.to_string()), rhs_span .diagnostic_label(LabelStyle::Secondary) .with_message(rhs_type.to_string()), ]), }; vec![d] } } impl ErrorDiagnostic for RuntimeError { fn diagnostics(&self) -> Vec { vec![Diagnostic::error() .with_message("runtime error") .with_notes(vec![format!("{self:#}")])] } } numbat-1.11.0/src/dimension.rs000064400000000000000000000157211046102023000143220ustar 00000000000000use crate::arithmetic::Power; use crate::ast::DimensionExpression; use crate::registry::{BaseRepresentation, Registry, Result}; #[derive(Default, Clone)] pub struct DimensionRegistry { registry: Registry<()>, } impl DimensionRegistry { pub fn get_base_representation( &self, expression: &DimensionExpression, ) -> Result { match expression { DimensionExpression::Unity(_) => Ok(BaseRepresentation::unity()), DimensionExpression::Dimension(_, name) => self .registry .get_base_representation_for_name(name) .map(|r| r.0), DimensionExpression::Multiply(_, lhs, rhs) => { let lhs = self.get_base_representation(lhs)?; let rhs = self.get_base_representation(rhs)?; Ok(lhs * rhs) } DimensionExpression::Divide(_, lhs, rhs) => { let lhs = self.get_base_representation(lhs)?; let rhs = self.get_base_representation(rhs)?; Ok(lhs / rhs) } DimensionExpression::Power(_, expr, _, outer_exponent) => { Ok(self.get_base_representation(expr)?.power(*outer_exponent)) } } } pub fn get_base_representation_for_name(&self, name: &str) -> Result { self.registry .get_base_representation_for_name(name) .map(|t| t.0) } pub fn get_derived_entry_names_for( &self, base_representation: &BaseRepresentation, ) -> Vec { self.registry .get_derived_entry_names_for(base_representation) } pub fn add_base_dimension(&mut self, name: &str) -> Result { self.registry.add_base_entry(name, ())?; Ok(self .registry .get_base_representation_for_name(name) .map(|t| t.0) .unwrap()) } pub fn add_derived_dimension( &mut self, name: &str, expression: &DimensionExpression, ) -> Result { let base_representation = self.get_base_representation(expression)?; self.registry .add_derived_entry(name, base_representation, ())?; Ok(self .registry .get_base_representation_for_name(name) .map(|t| t.0) .unwrap()) } pub fn contains(&self, dimension_name: &str) -> bool { self.registry.contains(dimension_name) } } #[test] fn basic() { use crate::arithmetic::Rational; use crate::parser::parse_dexpr; use crate::registry::BaseRepresentationFactor; let mut registry = DimensionRegistry::default(); registry.add_base_dimension("Length").unwrap(); registry.add_base_dimension("Time").unwrap(); registry .add_derived_dimension("Velocity", &parse_dexpr("Length / Time")) .unwrap(); registry .add_derived_dimension("Acceleration", &parse_dexpr("Length / Time^2")) .unwrap(); registry.add_base_dimension("Mass").unwrap(); registry .add_derived_dimension("Momentum", &parse_dexpr("Mass * Velocity")) .unwrap(); registry .add_derived_dimension("Energy", &parse_dexpr("Momentum^2 / Mass")) .unwrap(); assert_eq!( registry.get_base_representation(&parse_dexpr("Length")), Ok(BaseRepresentation::from_factor(BaseRepresentationFactor( "Length".into(), Rational::from_integer(1), ))) ); assert_eq!( registry.get_base_representation(&parse_dexpr("Time")), Ok(BaseRepresentation::from_factor(BaseRepresentationFactor( "Time".into(), Rational::from_integer(1) ))) ); assert_eq!( registry.get_base_representation(&parse_dexpr("Mass")), Ok(BaseRepresentation::from_factor(BaseRepresentationFactor( "Mass".into(), Rational::from_integer(1) ))) ); assert_eq!( registry.get_base_representation(&parse_dexpr("Velocity")), Ok(BaseRepresentation::from_factors([ BaseRepresentationFactor("Length".into(), Rational::from_integer(1)), BaseRepresentationFactor("Time".into(), Rational::from_integer(-1)) ])) ); assert_eq!( registry.get_base_representation(&parse_dexpr("Acceleration")), Ok(BaseRepresentation::from_factors([ BaseRepresentationFactor("Length".into(), Rational::from_integer(1)), BaseRepresentationFactor("Time".into(), Rational::from_integer(-2)) ])) ); assert_eq!( registry.get_base_representation(&parse_dexpr("Momentum")), Ok(BaseRepresentation::from_factors([ BaseRepresentationFactor("Length".into(), Rational::from_integer(1)), BaseRepresentationFactor("Mass".into(), Rational::from_integer(1)), BaseRepresentationFactor("Time".into(), Rational::from_integer(-1)) ])) ); assert_eq!( registry.get_base_representation(&parse_dexpr("Energy")), Ok(BaseRepresentation::from_factors([ BaseRepresentationFactor("Length".into(), Rational::from_integer(2)), BaseRepresentationFactor("Mass".into(), Rational::from_integer(1)), BaseRepresentationFactor("Time".into(), Rational::from_integer(-2)) ])) ); registry .add_derived_dimension("Momentum2", &parse_dexpr("Velocity * Mass")) .unwrap(); assert_eq!( registry.get_base_representation(&parse_dexpr("Momentum2")), Ok(BaseRepresentation::from_factors([ BaseRepresentationFactor("Length".into(), Rational::from_integer(1)), BaseRepresentationFactor("Mass".into(), Rational::from_integer(1)), BaseRepresentationFactor("Time".into(), Rational::from_integer(-1)) ])) ); registry .add_derived_dimension("Energy2", &parse_dexpr("Mass * Velocity^2")) .unwrap(); assert_eq!( registry.get_base_representation(&parse_dexpr("Energy2")), Ok(BaseRepresentation::from_factors([ BaseRepresentationFactor("Length".into(), Rational::from_integer(2)), BaseRepresentationFactor("Mass".into(), Rational::from_integer(1)), BaseRepresentationFactor("Time".into(), Rational::from_integer(-2)) ])) ); registry .add_derived_dimension("Velocity2", &parse_dexpr("Momentum / Mass")) .unwrap(); assert_eq!( registry.get_base_representation(&parse_dexpr("Velocity2")), Ok(BaseRepresentation::from_factors([ BaseRepresentationFactor("Length".into(), Rational::from_integer(1)), BaseRepresentationFactor("Time".into(), Rational::from_integer(-1)) ])) ); } #[test] fn fails_if_same_dimension_is_added_twice() { let mut registry = DimensionRegistry::default(); assert!(registry.add_base_dimension("Length").is_ok()); assert!(registry.add_base_dimension("Length").is_err()); } numbat-1.11.0/src/ffi.rs000064400000000000000000000610141046102023000130750ustar 00000000000000use std::collections::HashMap; use std::fmt::Write; use std::sync::OnceLock; use crate::currency::ExchangeRatesCache; use crate::datetime; use crate::interpreter::RuntimeError; use crate::pretty_print::PrettyPrint; use crate::value::{FunctionReference, Value}; use crate::vm::ExecutionContext; use crate::{ast::ProcedureKind, quantity::Quantity}; type ControlFlow = std::ops::ControlFlow; pub(crate) type ArityRange = std::ops::RangeInclusive; type Result = std::result::Result; type BoxedFunction = Box Result + Send + Sync>; pub(crate) enum Callable { Function(BoxedFunction), Procedure(fn(&mut ExecutionContext, &[Value]) -> ControlFlow), } pub(crate) struct ForeignFunction { pub(crate) name: String, pub(crate) arity: ArityRange, pub(crate) callable: Callable, } static FFI_PROCEDURES: OnceLock> = OnceLock::new(); static FFI_FUNCTIONS: OnceLock> = OnceLock::new(); pub(crate) fn procedures() -> &'static HashMap { FFI_PROCEDURES.get_or_init(|| { let mut m = HashMap::new(); m.insert( ProcedureKind::Print, ForeignFunction { name: "print".into(), arity: 0..=1, callable: Callable::Procedure(print), }, ); m.insert( ProcedureKind::Assert, ForeignFunction { name: "assert".into(), arity: 1..=1, callable: Callable::Procedure(assert), }, ); m.insert( ProcedureKind::AssertEq, ForeignFunction { name: "assert_eq".into(), arity: 2..=3, callable: Callable::Procedure(assert_eq), }, ); // Note: The 'type' procedure is missing here because it has special handling code in the compiler m }) } pub(crate) fn functions() -> &'static HashMap { FFI_FUNCTIONS.get_or_init(|| { let mut m = HashMap::new(); m.insert( "error".to_string(), ForeignFunction { name: "error".into(), arity: 1..=1, callable: Callable::Function(Box::new(error)), }, ); m.insert( "unit_of".to_string(), ForeignFunction { name: "unit_of".into(), arity: 1..=1, callable: Callable::Function(Box::new(unit_of)), }, ); m.insert( "abs".to_string(), ForeignFunction { name: "abs".into(), arity: 1..=1, callable: Callable::Function(Box::new(abs)), }, ); m.insert( "round".to_string(), ForeignFunction { name: "round".into(), arity: 1..=1, callable: Callable::Function(Box::new(round)), }, ); m.insert( "floor".to_string(), ForeignFunction { name: "floor".into(), arity: 1..=1, callable: Callable::Function(Box::new(floor)), }, ); m.insert( "ceil".to_string(), ForeignFunction { name: "ceil".into(), arity: 1..=1, callable: Callable::Function(Box::new(ceil)), }, ); m.insert( "sin".to_string(), ForeignFunction { name: "sin".into(), arity: 1..=1, callable: Callable::Function(Box::new(sin)), }, ); m.insert( "cos".to_string(), ForeignFunction { name: "cos".into(), arity: 1..=1, callable: Callable::Function(Box::new(cos)), }, ); m.insert( "tan".to_string(), ForeignFunction { name: "tan".into(), arity: 1..=1, callable: Callable::Function(Box::new(tan)), }, ); m.insert( "asin".to_string(), ForeignFunction { name: "asin".into(), arity: 1..=1, callable: Callable::Function(Box::new(asin)), }, ); m.insert( "acos".to_string(), ForeignFunction { name: "acos".into(), arity: 1..=1, callable: Callable::Function(Box::new(acos)), }, ); m.insert( "atan".to_string(), ForeignFunction { name: "atan".into(), arity: 1..=1, callable: Callable::Function(Box::new(atan)), }, ); m.insert( "atan2".to_string(), ForeignFunction { name: "atan2".into(), arity: 2..=2, callable: Callable::Function(Box::new(atan2)), }, ); m.insert( "sinh".to_string(), ForeignFunction { name: "sinh".into(), arity: 1..=1, callable: Callable::Function(Box::new(sinh)), }, ); m.insert( "cosh".to_string(), ForeignFunction { name: "cosh".into(), arity: 1..=1, callable: Callable::Function(Box::new(cosh)), }, ); m.insert( "tanh".to_string(), ForeignFunction { name: "tanh".into(), arity: 1..=1, callable: Callable::Function(Box::new(tanh)), }, ); m.insert( "asinh".to_string(), ForeignFunction { name: "asinh".into(), arity: 1..=1, callable: Callable::Function(Box::new(asinh)), }, ); m.insert( "acosh".to_string(), ForeignFunction { name: "acosh".into(), arity: 1..=1, callable: Callable::Function(Box::new(acosh)), }, ); m.insert( "atanh".to_string(), ForeignFunction { name: "atanh".into(), arity: 1..=1, callable: Callable::Function(Box::new(atanh)), }, ); m.insert( "mod".to_string(), ForeignFunction { name: "mod".into(), arity: 2..=2, callable: Callable::Function(Box::new(mod_)), }, ); m.insert( "exp".to_string(), ForeignFunction { name: "exp".into(), arity: 1..=1, callable: Callable::Function(Box::new(exp)), }, ); m.insert( "ln".to_string(), ForeignFunction { name: "ln".into(), arity: 1..=1, callable: Callable::Function(Box::new(ln)), }, ); m.insert( "log10".to_string(), ForeignFunction { name: "log10".into(), arity: 1..=1, callable: Callable::Function(Box::new(log10)), }, ); m.insert( "log2".to_string(), ForeignFunction { name: "log2".into(), arity: 1..=1, callable: Callable::Function(Box::new(log2)), }, ); m.insert( "gamma".to_string(), ForeignFunction { name: "gamma".into(), arity: 1..=1, callable: Callable::Function(Box::new(gamma)), }, ); m.insert( "mean".to_string(), ForeignFunction { name: "mean".into(), arity: 1..=usize::MAX, callable: Callable::Function(Box::new(mean)), }, ); m.insert( "maximum".to_string(), ForeignFunction { name: "maximum".into(), arity: 1..=usize::MAX, callable: Callable::Function(Box::new(maximum)), }, ); m.insert( "minimum".to_string(), ForeignFunction { name: "minimum".into(), arity: 1..=usize::MAX, callable: Callable::Function(Box::new(minimum)), }, ); m.insert( "exchange_rate".to_string(), ForeignFunction { name: "exchange_rate".into(), arity: 1..=1, callable: Callable::Function(Box::new(exchange_rate)), }, ); m.insert( "str_length".to_string(), ForeignFunction { name: "str_length".into(), arity: 1..=1, callable: Callable::Function(Box::new(str_length)), }, ); m.insert( "lowercase".to_string(), ForeignFunction { name: "lowercase".into(), arity: 1..=1, callable: Callable::Function(Box::new(lowercase)), }, ); m.insert( "uppercase".to_string(), ForeignFunction { name: "uppercase".into(), arity: 1..=1, callable: Callable::Function(Box::new(uppercase)), }, ); m.insert( "str_slice".to_string(), ForeignFunction { name: "str_slice".into(), arity: 3..=3, callable: Callable::Function(Box::new(str_slice)), }, ); m.insert( "chr".to_string(), ForeignFunction { name: "chr".into(), arity: 1..=1, callable: Callable::Function(Box::new(chr)), }, ); m.insert( "now".to_string(), ForeignFunction { name: "now".into(), arity: 0..=0, callable: Callable::Function(Box::new(now)), }, ); m.insert( "datetime".to_string(), ForeignFunction { name: "datetime".into(), arity: 1..=1, callable: Callable::Function(Box::new(datetime)), }, ); m.insert( "format_datetime".to_string(), ForeignFunction { name: "format_datetime".into(), arity: 2..=2, callable: Callable::Function(Box::new(format_datetime)), }, ); m.insert( "get_local_timezone".to_string(), ForeignFunction { name: "get_local_timezone".into(), arity: 0..=0, callable: Callable::Function(Box::new(get_local_timezone)), }, ); m.insert( "tz".to_string(), ForeignFunction { name: "tz".into(), arity: 1..=1, callable: Callable::Function(Box::new(tz)), }, ); m.insert( "unixtime".to_string(), ForeignFunction { name: "unixtime".into(), arity: 1..=1, callable: Callable::Function(Box::new(unixtime)), }, ); m.insert( "from_unixtime".to_string(), ForeignFunction { name: "from_unixtime".into(), arity: 1..=1, callable: Callable::Function(Box::new(from_unixtime)), }, ); m }) } fn print(ctx: &mut ExecutionContext, args: &[Value]) -> ControlFlow { assert!(args.len() <= 1); if args.is_empty() { (ctx.print_fn)(&crate::markup::text("")) } else { match &args[0] { Value::String(string) => (ctx.print_fn)(&crate::markup::text(string)), // print string without quotes arg => (ctx.print_fn)(&arg.pretty_print()), } } ControlFlow::Continue(()) } fn assert(_: &mut ExecutionContext, args: &[Value]) -> ControlFlow { assert!(args.len() == 1); if args[0].unsafe_as_bool() { ControlFlow::Continue(()) } else { ControlFlow::Break(RuntimeError::AssertFailed) } } fn assert_eq(_: &mut ExecutionContext, args: &[Value]) -> ControlFlow { assert!(args.len() == 2 || args.len() == 3); let lhs = args[0].unsafe_as_quantity(); let rhs = args[1].unsafe_as_quantity(); if args.len() == 2 { let error = ControlFlow::Break(RuntimeError::AssertEq2Failed(lhs.clone(), rhs.clone())); if let Ok(args1_converted) = rhs.convert_to(lhs.unit()) { if lhs == &args1_converted { ControlFlow::Continue(()) } else { error } } else { error } } else { let result = lhs - rhs; let eps = args[2].unsafe_as_quantity(); match result { Ok(diff) => match diff.convert_to(eps.unit()) { Err(e) => ControlFlow::Break(RuntimeError::QuantityError(e)), Ok(diff_converted) => { if diff_converted.unsafe_value().to_f64().abs() < eps.unsafe_value().to_f64() { ControlFlow::Continue(()) } else { ControlFlow::Break(RuntimeError::AssertEq3Failed( lhs.clone(), rhs.clone(), eps.clone(), )) } } }, Err(e) => ControlFlow::Break(RuntimeError::QuantityError(e)), } } } fn error(args: &[Value]) -> Result { assert!(args.len() == 1); Err(RuntimeError::UserError(args[0].unsafe_as_string().into())) } fn unit_of(args: &[Value]) -> Result { assert!(args.len() == 1); Ok(Value::Quantity(Quantity::new_f64( 1.0, args[0].unsafe_as_quantity().unit().clone(), ))) } fn abs(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let value = arg.unsafe_value().to_f64(); Ok(Value::Quantity(Quantity::new_f64( value.abs(), arg.unit().clone(), ))) } fn round(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let value = arg.unsafe_value().to_f64(); Ok(Value::Quantity(Quantity::new_f64( value.round(), arg.unit().clone(), ))) } fn floor(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let value = arg.unsafe_value().to_f64(); Ok(Value::Quantity(Quantity::new_f64( value.floor(), arg.unit().clone(), ))) } fn ceil(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let value = arg.unsafe_value().to_f64(); Ok(Value::Quantity(Quantity::new_f64( value.ceil(), arg.unit().clone(), ))) } fn sin(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.sin()))) } fn cos(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.cos()))) } fn tan(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.tan()))) } fn asin(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.asin()))) } fn acos(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.acos()))) } fn atan(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.atan()))) } fn atan2(args: &[Value]) -> Result { assert!(args.len() == 2); let y = args[0].unsafe_as_quantity(); let x = args[1].unsafe_as_quantity(); let input0 = y.unsafe_value().to_f64(); let input1 = x.convert_to(y.unit()).unwrap().unsafe_value().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input0.atan2(input1)))) } fn sinh(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.sinh()))) } fn cosh(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.cosh()))) } fn tanh(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.tanh()))) } fn asinh(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.asinh()))) } fn acosh(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.acosh()))) } fn atanh(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.atanh()))) } fn mod_(args: &[Value]) -> Result { assert!(args.len() == 2); let x = args[0].unsafe_as_quantity(); let y = args[1].unsafe_as_quantity(); let input0 = x.unsafe_value().to_f64(); let input1 = y.convert_to(x.unit()).unwrap().unsafe_value().to_f64(); Ok(Value::Quantity(Quantity::new_f64( input0.rem_euclid(input1), x.unit().clone(), ))) } fn exp(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.exp()))) } fn ln(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.ln()))) } fn log10(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.log10()))) } fn log2(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(input.log2()))) } fn gamma(args: &[Value]) -> Result { assert!(args.len() == 1); let arg = args[0].unsafe_as_quantity(); let input = arg.as_scalar().unwrap().to_f64(); Ok(Value::Quantity(Quantity::from_scalar(crate::gamma::gamma( input, )))) } fn mean(args: &[Value]) -> Result { assert!(!args.is_empty()); let output_unit = args[0].unsafe_as_quantity().unit(); Ok(Value::Quantity(Quantity::new_f64( args.iter() .map(|q| { q.unsafe_as_quantity() .convert_to(output_unit) .unwrap() .unsafe_value() .to_f64() }) .sum::() / (args.len() as f64), output_unit.clone(), ))) } fn maximum(args: &[Value]) -> Result { assert!(!args.is_empty()); let output_unit = args[0].unsafe_as_quantity().unit(); Ok(Value::Quantity(Quantity::new( args.iter() .map(|q| { *q.unsafe_as_quantity() .convert_to(output_unit) .unwrap() .unsafe_value() }) .max_by(|l, r| l.partial_cmp(r).unwrap()) .unwrap(), output_unit.clone(), ))) } fn minimum(args: &[Value]) -> Result { assert!(!args.is_empty()); let output_unit = args[0].unsafe_as_quantity().unit(); Ok(Value::Quantity(Quantity::new( args.iter() .map(|q| { *q.unsafe_as_quantity() .convert_to(output_unit) .unwrap() .unsafe_value() }) .min_by(|l, r| l.partial_cmp(r).unwrap()) .unwrap(), output_unit.clone(), ))) } fn exchange_rate(args: &[Value]) -> Result { assert!(args.len() == 1); let rate = args[0].unsafe_as_string(); let exchange_rates = ExchangeRatesCache::new(); Ok(Value::Quantity(Quantity::from_scalar( exchange_rates.get_rate(rate).unwrap_or(f64::NAN), ))) } fn str_length(args: &[Value]) -> Result { assert!(args.len() == 1); let len = args[0].unsafe_as_string().len(); Ok(Value::Quantity(Quantity::from_scalar(len as f64))) } fn lowercase(args: &[Value]) -> Result { assert!(args.len() == 1); Ok(Value::String(args[0].unsafe_as_string().to_lowercase())) } fn uppercase(args: &[Value]) -> Result { assert!(args.len() == 1); Ok(Value::String(args[0].unsafe_as_string().to_uppercase())) } fn str_slice(args: &[Value]) -> Result { assert!(args.len() == 3); let input = args[0].unsafe_as_string(); let start = args[1].unsafe_as_quantity().unsafe_value().to_f64() as usize; let end = args[2].unsafe_as_quantity().unsafe_value().to_f64() as usize; let output = input.get(start..end).unwrap_or_default(); Ok(Value::String(output.into())) } fn chr(args: &[Value]) -> Result { assert!(args.len() == 1); let idx = args[0].unsafe_as_quantity().unsafe_value().to_f64() as u32; let output = char::from_u32(idx).unwrap_or('�'); Ok(Value::String(output.to_string())) } fn now(args: &[Value]) -> Result { assert!(args.is_empty()); let now = chrono::Local::now().fixed_offset(); Ok(Value::DateTime(now)) } fn datetime(args: &[Value]) -> Result { assert!(args.len() == 1); let input = args[0].unsafe_as_string(); let output = datetime::parse_datetime(input) .ok_or(RuntimeError::DateParsingErrorUnknown)? .fixed_offset(); Ok(Value::DateTime(output)) } fn format_datetime(args: &[Value]) -> Result { assert!(args.len() == 2); let format = args[0].unsafe_as_string(); let dt = args[1].unsafe_as_datetime(); let mut output = String::new(); write!(output, "{}", dt.format(format)).map_err(|_| RuntimeError::DateFormattingError)?; Ok(Value::String(output)) } fn get_local_timezone(args: &[Value]) -> Result { assert!(args.is_empty()); let local_tz = datetime::get_local_timezone_or_utc().to_string(); Ok(Value::String(local_tz)) } fn tz(args: &[Value]) -> Result { assert!(args.len() == 1); let tz = args[0].unsafe_as_string(); Ok(Value::FunctionReference(FunctionReference::TzConversion( tz.into(), ))) } fn unixtime(args: &[Value]) -> Result { assert!(args.len() == 1); let input = args[0].unsafe_as_datetime(); let output = input.timestamp(); Ok(Value::Quantity(Quantity::from_scalar(output as f64))) } fn from_unixtime(args: &[Value]) -> Result { assert!(args.len() == 1); let timestamp = args[0].unsafe_as_quantity().unsafe_value().to_f64() as i64; let dt = chrono::DateTime::from_timestamp(timestamp, 0) .ok_or(RuntimeError::DateTimeOutOfRange)? .with_timezone(&datetime::get_local_timezone_or_utc()) .fixed_offset(); Ok(Value::DateTime(dt)) } numbat-1.11.0/src/gamma.rs000064400000000000000000000004631046102023000134140ustar 00000000000000use std::ffi::c_double; extern "C" { fn tgamma(n: c_double) -> c_double; } // TODO: This will be part of the standard in the future [1] [2] // [1] https://github.com/rust-lang/rfcs/issues/864 // [2] https://github.com/rust-lang/rust/pull/99747 pub fn gamma(x: f64) -> f64 { unsafe { tgamma(x) } } numbat-1.11.0/src/help.rs000064400000000000000000000045641046102023000132700ustar 00000000000000/// Print a help, linking the documentation, and live-running some examples /// in an isolated context. use crate::markup as m; use crate::module_importer::BuiltinModuleImporter; use crate::resolver::CodeSource; use crate::Context; use crate::InterpreterSettings; use std::sync::{Arc, Mutex}; fn evaluate_example(context: &mut Context, input: &str) -> m::Markup { let statement_output: Arc>> = Arc::new(Mutex::new(vec![])); let statement_output_c = statement_output.clone(); let mut settings = InterpreterSettings { print_fn: Box::new(move |s: &m::Markup| { statement_output_c.lock().unwrap().push(s.clone()); }), }; let (statements, interpreter_result) = context .interpret_with_settings(&mut settings, input, CodeSource::Internal) .expect("No error in 'help' examples"); let markup = statement_output .lock() .unwrap() .iter() .fold(m::empty(), |accumulated_mk, single_line| { accumulated_mk + m::nl() + m::whitespace(" ") + single_line.clone() + m::nl() }) + interpreter_result.to_markup( statements.last(), context.dimension_registry(), true, true, ); markup } pub fn help_markup() -> m::Markup { let mut output = m::nl() + m::text("Numbat is a statically typed programming language for scientific computations") + m::nl() + m::text("with first class support for physical dimensions and units. Please refer to") + m::nl() + m::text("the full documentation online at ") + m::string("https://numbat.dev/doc/") + m::text(" or try one of these ") + m::nl() + m::text("examples:") + m::nl() + m::nl(); let examples = [ "8 km / (1 h + 25 min)", "atan2(30 cm, 1 m) -> deg", "let ω = 2 π c / 660 nm", r#"print("Energy of red photons: {ℏ ω -> eV}")"#, ]; let mut example_context = Context::new(BuiltinModuleImporter::default()); let _use_prelude_output = evaluate_example(&mut example_context, "use prelude"); for example in examples.iter() { output += m::text(">>> ") + m::text(example) + m::nl(); output += evaluate_example(&mut example_context, example) + m::nl(); } output } numbat-1.11.0/src/html_formatter.rs000064400000000000000000000064001046102023000153560ustar 00000000000000use crate::buffered_writer::BufferedWriter; use crate::markup::{FormatType, FormattedString, Formatter}; use termcolor::{Color, WriteColor}; pub struct HtmlFormatter; pub fn html_format(class: Option<&str>, content: &str) -> String { if content.is_empty() { return "".into(); } let content = html_escape::encode_text(content); if let Some(class) = class { format!("{content}") } else { content.into() } } impl Formatter for HtmlFormatter { fn format_part( &self, FormattedString(_output_type, format_type, s): &FormattedString, ) -> String { let css_class = match format_type { FormatType::Whitespace => None, FormatType::Emphasized => Some("emphasized"), FormatType::Dimmed => Some("dimmed"), FormatType::Text => None, FormatType::String => Some("string"), FormatType::Keyword => Some("keyword"), FormatType::Value => Some("value"), FormatType::Unit => Some("unit"), FormatType::Identifier => Some("identifier"), FormatType::TypeIdentifier => Some("type-identifier"), FormatType::Operator => Some("operator"), FormatType::Decorator => Some("decorator"), }; html_format(css_class, s) } } pub struct HtmlWriter { buffer: Vec, color: Option, } impl HtmlWriter { pub fn new() -> Self { HtmlWriter { buffer: vec![], color: None, } } } impl BufferedWriter for HtmlWriter { fn to_string(&self) -> String { String::from_utf8_lossy(&self.buffer).into() } } impl std::io::Write for HtmlWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { if let Some(color) = &self.color { if color.fg() == Some(&Color::Red) { self.buffer .write("".as_bytes())?; let size = self.buffer.write(buf)?; self.buffer.write("".as_bytes())?; Ok(size) } else if color.fg() == Some(&Color::Blue) { self.buffer .write("".as_bytes())?; let size = self.buffer.write(buf)?; self.buffer.write("".as_bytes())?; Ok(size) } else if color.bold() { self.buffer .write("".as_bytes())?; let size = self.buffer.write(buf)?; self.buffer.write("".as_bytes())?; Ok(size) } else { self.buffer.write(buf) } } else { self.buffer.write(buf) } } fn flush(&mut self) -> std::io::Result<()> { self.buffer.flush() } } impl WriteColor for HtmlWriter { fn supports_color(&self) -> bool { true } fn set_color(&mut self, spec: &termcolor::ColorSpec) -> std::io::Result<()> { self.color = Some(spec.clone()); Ok(()) } fn reset(&mut self) -> std::io::Result<()> { self.color = None; Ok(()) } } numbat-1.11.0/src/interpreter.rs000064400000000000000000000262321046102023000146770ustar 00000000000000use crate::{ dimension::DimensionRegistry, markup::Markup, pretty_print::PrettyPrint, quantity::{Quantity, QuantityError}, typed_ast::{Statement, Type}, unit_registry::{UnitRegistry, UnitRegistryError}, }; use crate::markup as m; use thiserror::Error; pub use crate::value::Value; #[derive(Debug, Clone, Error, PartialEq, Eq)] pub enum RuntimeError { #[error("Division by zero")] DivisionByZero, #[error("Expected factorial argument to be a non-negative integer")] FactorialOfNegativeNumber, #[error("Expected factorial argument to be a finite integer number")] FactorialOfNonInteger, #[error("{0}")] UnitRegistryError(UnitRegistryError), // TODO: can this even be triggered? #[error("{0}")] QuantityError(QuantityError), #[error("Assertion failed")] AssertFailed, #[error( "Assertion failed because the following two quantities are not the same:\n {0}\n {1}" )] AssertEq2Failed(Quantity, Quantity), #[error("Assertion failed because the following two quantities differ by more than {2}:\n {0}\n {1}")] AssertEq3Failed(Quantity, Quantity, Quantity), #[error("Could not load exchange rates from European Central Bank.")] CouldNotLoadExchangeRates, #[error("User error: {0}")] UserError(String), #[error("Unrecognized datetime format")] DateParsingErrorUnknown, #[error("Unknown timezone: {0}")] UnknownTimezone(String), #[error("Exceeded maximum size for time durations")] DurationOutOfRange, #[error("DateTime out of range")] DateTimeOutOfRange, #[error("Error in datetime format. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for possible format specifiers.")] DateFormattingError, } #[derive(Debug, PartialEq, Eq)] #[must_use] pub enum InterpreterResult { Value(Value), Continue, } impl InterpreterResult { pub fn to_markup( &self, evaluated_statement: Option<&Statement>, registry: &DimensionRegistry, with_type_info: bool, with_equal_sign: bool, ) -> Markup { match self { Self::Value(value) => { let leader = if with_equal_sign { m::whitespace(" ") + m::operator("=") + m::space() } else { m::empty() }; let type_markup = if with_type_info { evaluated_statement .and_then(Statement::as_expression) .and_then(|e| { if e.get_type() == Type::scalar() { None } else { let ty = e.get_type().to_readable_type(registry); Some(m::dimmed(" [") + ty + m::dimmed("]")) } }) .unwrap_or_else(m::empty) } else { m::empty() }; leader + value.pretty_print() + type_markup + m::nl() } Self::Continue => m::empty(), } } /// Returns `true` if the interpreter result is [`Value`]. /// /// [`Value`]: InterpreterResult::Value #[must_use] pub fn is_value(&self) -> bool { matches!(self, Self::Value(..)) } /// Returns `true` if the interpreter result is [`Continue`]. /// /// [`Continue`]: InterpreterResult::Continue #[must_use] pub fn is_continue(&self) -> bool { matches!(self, Self::Continue) } pub fn value_as_string(&self) -> Option { match self { Self::Continue => None, Self::Value(value) => Some(value.to_string()), } } } pub type Result = std::result::Result; pub type PrintFunction = dyn FnMut(&Markup) + Send; pub struct InterpreterSettings { pub print_fn: Box, } impl Default for InterpreterSettings { fn default() -> Self { Self { print_fn: Box::new(move |s: &Markup| { print!("{}", s); }), } } } pub trait Interpreter { fn new() -> Self; fn interpret_statements( &mut self, settings: &mut InterpreterSettings, statements: &[Statement], ) -> Result; fn get_unit_registry(&self) -> &UnitRegistry; } #[cfg(test)] mod tests { use crate::prefix_parser::AcceptsPrefix; use crate::unit::{CanonicalName, Unit}; use crate::{bytecode_interpreter::BytecodeInterpreter, prefix_transformer::Transformer}; use super::*; static TEST_PRELUDE: &str = " dimension Scalar = 1 dimension Length dimension Time dimension Mass dimension Velocity = Length / Time dimension Momentum = Mass * Velocity dimension Frequency = 1 / Time @metric_prefixes @aliases(m: short) unit meter : Length unit alternative_length_base_unit: Length # maybe this should be disallowed @aliases(s: short) unit second : Time @aliases(Hz: short) unit hertz: Frequency = 1 / second fn sin(x: Scalar) -> Scalar fn atan2(y: D, x: D) -> Scalar fn mean(xs: D…) -> D fn maximum(xs: D…) -> D fn minimum(xs: D…) -> D"; fn get_interpreter_result(input: &str) -> Result { let full_code = format!("{prelude}\n{input}", prelude = TEST_PRELUDE, input = input); let statements = crate::parser::parse(&full_code, 0) .expect("No parse errors for inputs in this test suite"); let statements_transformed = Transformer::new() .transform(statements) .expect("No name resolution errors for inputs in this test suite"); let statements_typechecked = crate::typechecker::TypeChecker::default() .check_statements(statements_transformed) .expect("No type check errors for inputs in this test suite"); BytecodeInterpreter::new() .interpret_statements(&mut InterpreterSettings::default(), &statements_typechecked) } #[track_caller] fn assert_evaluates_to(input: &str, expected: Quantity) { if let InterpreterResult::Value(actual) = get_interpreter_result(input).unwrap() { let actual = actual.unsafe_as_quantity(); assert_eq!(actual, &expected); } else { panic!(); } } #[track_caller] fn assert_evaluates_to_scalar(input: &str, expected: f64) { assert_evaluates_to(input, Quantity::from_scalar(expected)) } #[track_caller] fn assert_runtime_error(input: &str, err_expected: RuntimeError) { if let Err(err_actual) = get_interpreter_result(input) { assert_eq!(err_actual, err_expected); } else { panic!(); } } #[test] fn simple_arithmetic() { assert_evaluates_to_scalar("0", 0.0); assert_evaluates_to_scalar("1", 1.0); assert_evaluates_to_scalar("1+2", 1.0 + 2.0); assert_evaluates_to_scalar("-1", -1.0); assert_evaluates_to_scalar("2+3*4", 2.0 + 3.0 * 4.0); assert_evaluates_to_scalar("2*3+4", 2.0 * 3.0 + 4.0); assert_evaluates_to_scalar("(2+3)*4", (2.0 + 3.0) * 4.0); assert_evaluates_to_scalar("(2/3)*4", (2.0 / 3.0) * 4.0); assert_evaluates_to_scalar("-2 * 3", -2.0 * 3.0); assert_evaluates_to_scalar("2 * -3", 2.0 * -3.0); assert_evaluates_to_scalar("2 - 3 - 4", 2.0 - 3.0 - 4.0); assert_evaluates_to_scalar("2 - -3", 2.0 - -3.0); assert_evaluates_to_scalar("+2 * 3", 2.0 * 3.0); assert_evaluates_to_scalar("2 * +3", 2.0 * 3.0); assert_evaluates_to_scalar("+2 - +3", 2.0 - 3.0); } #[test] fn comparisons() { assert_evaluates_to_scalar("if 2 meter > 150 cm then 1 else 0", 1.0); assert_runtime_error( "1 meter > alternative_length_base_unit", RuntimeError::QuantityError(QuantityError::IncompatibleUnits( Unit::new_base( "meter", CanonicalName::new("m", AcceptsPrefix::only_short()), ), Unit::new_base( "alternative_length_base_unit", CanonicalName::new("alternative_length_base_unit", AcceptsPrefix::only_long()), ), )), ); } #[test] fn arithmetic_with_units() { use crate::unit::Unit; assert_evaluates_to( "2 meter + 3 meter", Quantity::from_scalar(2.0 + 3.0) * Quantity::from_unit(Unit::meter()), ); assert_evaluates_to( "dimension Pixel @aliases(px: short) unit pixel : Pixel 2 * pixel", Quantity::from_scalar(2.0) * Quantity::from_unit(Unit::new_base( "pixel", CanonicalName::new("px", AcceptsPrefix::only_short()), )), ); assert_evaluates_to( "fn speed(distance: Length, time: Time) -> Velocity = distance / time speed(10 * meter, 2 * second)", Quantity::from_scalar(5.0) * (Quantity::from_unit(Unit::meter()) / Quantity::from_unit(Unit::second())), ); } #[test] fn power_operator() { assert_evaluates_to_scalar("2^3", 2.0f64.powf(3.0)); assert_evaluates_to_scalar("-2^4", -(2.0f64.powf(4.0))); assert_evaluates_to_scalar("2^(-3)", 2.0f64.powf(-3.0)); } #[test] fn multiline_input_yields_result_of_last_line() { assert_evaluates_to_scalar("2\n3", 3.0); } #[test] fn variable_definitions() { assert_evaluates_to_scalar("let x = 2\nlet y = 3\nx + y", 2.0 + 3.0); } #[test] fn function_definitions() { assert_evaluates_to_scalar("fn f(x: Scalar) = 2 * x + 3\nf(5)", 2.0 * 5.0 + 3.0); } #[test] fn foreign_functions() { assert_evaluates_to_scalar("sin(1)", 1.0f64.sin()); assert_evaluates_to_scalar("atan2(2 meter, 1 meter)", 2.0f64.atan2(1.0f64)); } #[test] fn statistics_functions() { assert_evaluates_to_scalar("mean(1, 1, 1, 0)", 0.75); assert_evaluates_to( "mean(1 m, 1 m, 1 m, 0 m)", Quantity::new_f64(0.75, Unit::meter()), ); assert_evaluates_to("mean(2 m, 100 cm)", Quantity::new_f64(1.5, Unit::meter())); assert_evaluates_to_scalar("maximum(1, 2, 0, -3)", 2.0); assert_evaluates_to( "maximum(2 m, 0.1 km)", Quantity::new_f64(100.0, Unit::meter()), ); assert_evaluates_to_scalar("minimum(1, 2, 0, -3)", -3.0); assert_evaluates_to( "minimum(2 m, 150 cm)", Quantity::new_f64(1.5, Unit::meter()), ); } #[test] fn division_by_zero_raises_runtime_error() { assert_runtime_error("1/0", RuntimeError::DivisionByZero); } #[test] fn non_rational_exponent() { // Regression test, found using fuzzing assert_runtime_error( "0**0⁻⁸", RuntimeError::QuantityError(QuantityError::NonRationalExponent), ); } } numbat-1.11.0/src/keywords.rs000064400000000000000000000011441046102023000141760ustar 00000000000000/// This is used for tab-completion and highlighting, /// not for tokenizing/parsing. pub const KEYWORDS: &[&str] = &[ // keywords which are followed by a space "per ", "to ", "let ", "fn ", "dimension ", "unit ", "use ", // 'inline' keywords "long", "short", "both", "none", "if", "then", "else", "Bool", "true", "false", "String", "DateTime", // decorators "metric_prefixes", "binary_prefixes", "aliases", "name", "url", // procedures "print(", "assert_eq(", "assert(", "type(", ]; numbat-1.11.0/src/lib.rs000064400000000000000000000600471046102023000131040ustar 00000000000000mod arithmetic; mod ast; #[cfg(feature = "html-formatter")] pub mod buffered_writer; mod bytecode_interpreter; mod column_formatter; mod currency; mod datetime; mod decorator; pub mod diagnostic; mod dimension; mod ffi; mod gamma; pub mod help; #[cfg(feature = "html-formatter")] pub mod html_formatter; mod interpreter; pub mod keywords; pub mod markup; mod math; pub mod module_importer; mod name_resolution; mod number; mod parser; mod prefix; mod prefix_parser; mod prefix_transformer; pub mod pretty_print; mod product; mod quantity; mod registry; pub mod resolver; mod span; mod suggestion; mod tokenizer; mod typechecker; mod typed_ast; pub mod unicode_input; mod unit; mod unit_registry; pub mod value; mod vm; use bytecode_interpreter::BytecodeInterpreter; use column_formatter::ColumnFormatter; use currency::ExchangeRatesCache; use diagnostic::ErrorDiagnostic; use dimension::DimensionRegistry; use interpreter::Interpreter; use keywords::KEYWORDS; use markup as m; use markup::FormatType; use markup::Markup; use module_importer::{ModuleImporter, NullImporter}; use prefix_transformer::Transformer; use resolver::CodeSource; use resolver::Resolver; use resolver::ResolverError; use thiserror::Error; use typechecker::{TypeCheckError, TypeChecker}; pub use diagnostic::Diagnostic; pub use interpreter::InterpreterResult; pub use interpreter::InterpreterSettings; pub use interpreter::RuntimeError; pub use name_resolution::NameResolutionError; pub use parser::ParseError; pub use registry::BaseRepresentation; pub use registry::BaseRepresentationFactor; pub use typed_ast::Statement; pub use typed_ast::Type; use unit::BaseUnitAndFactor; use unit_registry::UnitMetadata; use crate::prefix_parser::PrefixParserResult; use crate::unicode_input::UNICODE_INPUT; #[derive(Debug, Clone, Error)] pub enum NumbatError { #[error("{0}")] ResolverError(ResolverError), #[error("{0}")] NameResolutionError(NameResolutionError), #[error("{0}")] TypeCheckError(TypeCheckError), #[error("{0}")] RuntimeError(RuntimeError), } type Result = std::result::Result; #[derive(Clone)] pub struct Context { prefix_transformer: Transformer, typechecker: TypeChecker, interpreter: BytecodeInterpreter, resolver: Resolver, load_currency_module_on_demand: bool, terminal_width: Option, } impl Context { pub fn new(module_importer: impl ModuleImporter + Send + Sync + 'static) -> Self { Context { prefix_transformer: Transformer::new(), typechecker: TypeChecker::default(), interpreter: BytecodeInterpreter::new(), resolver: Resolver::new(module_importer), load_currency_module_on_demand: false, terminal_width: None, } } pub fn new_without_importer() -> Self { Self::new(NullImporter::default()) } pub fn set_debug(&mut self, activate: bool) { self.interpreter.set_debug(activate); } pub fn load_currency_module_on_demand(&mut self, yes: bool) { self.load_currency_module_on_demand = yes; } /// Fill the currency exchange rate cache. This call is blocking. pub fn prefetch_exchange_rates() { let _unused = ExchangeRatesCache::fetch(); } pub fn set_exchange_rates(xml_content: &str) { ExchangeRatesCache::set_from_xml(xml_content); } pub fn variable_names(&self) -> impl Iterator + '_ { self.prefix_transformer .variable_names .iter() .filter(|name| !name.starts_with('_')) .cloned() } pub fn function_names(&self) -> impl Iterator + '_ { self.prefix_transformer .function_names .iter() .filter(|name| !name.starts_with('_')) .cloned() } pub fn unit_names(&self) -> &[Vec] { &self.prefix_transformer.unit_names } pub fn dimension_names(&self) -> &[String] { &self.prefix_transformer.dimension_names } pub fn print_environment(&self) -> Markup { let mut functions: Vec<_> = self.function_names().collect(); functions.sort(); let mut dimensions = Vec::from(self.dimension_names()); dimensions.sort(); let mut units = Vec::from(self.unit_names()); units.sort(); let mut variables: Vec<_> = self.variable_names().collect(); variables.sort(); let mut output = m::empty(); output += m::emphasized("List of functions:") + m::nl(); output += self.print_functions() + m::nl(); output += m::emphasized("List of dimensions:") + m::nl(); output += self.print_dimensions() + m::nl(); output += m::emphasized("List of units:") + m::nl(); output += self.print_units() + m::nl(); output += m::emphasized("List of variables:") + m::nl(); output += self.print_variables() + m::nl(); output } fn print_sorted(&self, mut entries: Vec, format_type: FormatType) -> Markup { entries.sort_by_key(|e| e.to_lowercase()); let formatter = ColumnFormatter::new(self.terminal_width.unwrap_or(80)); formatter.format(entries, format_type) } pub fn print_functions(&self) -> Markup { self.print_sorted(self.function_names().collect(), FormatType::Identifier) } pub fn print_dimensions(&self) -> Markup { self.print_sorted(self.dimension_names().into(), FormatType::TypeIdentifier) } pub fn print_variables(&self) -> Markup { self.print_sorted(self.variable_names().collect(), FormatType::Identifier) } pub fn print_units(&self) -> Markup { let units = self.unit_names().iter().flatten().cloned().collect(); self.print_sorted(units, FormatType::Unit) } /// Gets completions for the given word_part /// /// If `add_paren` is true, then an opening paren will be added to the end of function names pub fn get_completions_for<'a>( &self, word_part: &'a str, add_paren: bool, ) -> impl Iterator + 'a { const COMMON_METRIC_PREFIXES: &[&str] = &[ "pico", "nano", "micro", "milli", "centi", "kilo", "mega", "giga", "tera", ]; let metric_prefixes: Vec<_> = COMMON_METRIC_PREFIXES .iter() .filter(|prefix| { word_part.starts_with(*prefix) || (!word_part.is_empty() && prefix.starts_with(word_part)) }) .collect(); let mut words: Vec<_> = KEYWORDS.iter().map(|k| k.to_string()).collect(); for (patterns, _) in UNICODE_INPUT { for pattern in *patterns { words.push(pattern.to_string()); } } { for variable in self.variable_names() { words.push(variable.clone()); } for function in self.function_names() { if add_paren { words.push(format!("{}(", function)); } else { words.push(function.to_string()); } } for dimension in self.dimension_names() { words.push(dimension.clone()); } for (_, (_, meta)) in self.unit_representations() { for (unit, accepts_prefix) in meta.aliases { words.push(unit.clone()); // Add some of the common long prefixes for units that accept them. // We do not add all possible prefixes here in order to keep the // number of completions to a reasonable size. Also, we do not add // short prefixes for units that accept them, as that leads to lots // and lots of 2-3 character words. if accepts_prefix.long && meta.metric_prefixes { for prefix in &metric_prefixes { words.push(format!("{prefix}{unit}")); } } } } } words.sort(); words.dedup(); words.into_iter().filter(move |w| w.starts_with(word_part)) } pub fn print_info_for_keyword(&mut self, keyword: &str) -> Markup { let url_encode = |s: &str| s.replace('(', "%28").replace(')', "%29"); if keyword.is_empty() { return m::text("Usage: info "); } let reg = self.interpreter.get_unit_registry(); if let PrefixParserResult::UnitIdentifier(_span, prefix, _, full_name) = self.prefix_transformer.prefix_parser.parse(keyword) { if let Some(md) = reg .inner .get_base_representation_for_name(&full_name) .ok() .map(|(_, md)| md) { let mut help = m::text("Unit: ") + m::unit(md.name.as_deref().unwrap_or(keyword)); if let Some(url) = &md.url { help += m::text(" (") + m::string(url_encode(url)) + m::text(")"); } help += m::nl(); if md.aliases.len() > 1 { help += m::text("Aliases: ") + m::text( md.aliases .iter() .map(|(x, _)| x.as_str()) .collect::>() .join(", "), ) + m::nl(); } if matches!(md.type_, Type::Dimension(d) if d.is_scalar()) { help += m::text("A dimensionless unit ([") + md.readable_type + m::text("])") + m::nl(); } else { help += m::text("A unit of [") + md.readable_type + m::text("]") + m::nl(); } if let Some(defining_info) = self.interpreter.get_defining_unit(&full_name) { let x = defining_info .iter() .filter(|u| !u.unit_id.is_base()) .map(|unit_factor| unit_factor.unit_id.unit_and_factor()) .next(); if !prefix.is_none() { help += m::nl() + m::value("1 ") + m::unit(keyword) + m::text(" = ") + m::value(prefix.factor().pretty_print()) + m::space() + m::unit(&full_name); } if let Some(BaseUnitAndFactor(prod, num)) = x { help += m::nl() + m::value("1 ") + m::unit(&full_name) + m::text(" = ") + m::value(num.pretty_print()) + m::space() + prod.pretty_print_with( |f| f.exponent, 'x', '/', true, Some(m::FormatType::Unit), ); } else { help += m::nl() + m::unit(&full_name) + m::text(" is a base unit"); } }; help += m::nl(); return help; } }; if let Some(l) = self.interpreter.lookup_global(keyword) { let mut help = m::text("Variable: "); if let Some(name) = &l.metadata.name { help += m::text(name); } else { help += m::identifier(keyword); } if let Some(url) = &l.metadata.url { help += m::text(" (") + m::string(url_encode(url)) + m::text(")"); } help += m::nl(); if l.metadata.aliases.len() > 1 { help += m::text("Aliases: ") + m::text( l.metadata .aliases .iter() .map(|x| x.as_str()) .collect::>() .join(", "), ) + m::nl(); } if let Ok((_, results)) = self.interpret(keyword, CodeSource::Internal) { help += m::nl() + results.to_markup(None, self.dimension_registry(), true, true); } return help; } m::text("Not found") } pub fn list_modules(&self) -> impl Iterator { let modules = self.resolver.get_importer().list_modules(); modules.into_iter().map(|m| m.0.join("::")) } pub fn dimension_registry(&self) -> &DimensionRegistry { self.typechecker.registry() } pub fn base_units(&self) -> impl Iterator + '_ { self.interpreter .get_unit_registry() .inner .iter_base_entries() } pub fn unit_representations( &self, ) -> impl Iterator + '_ { let registry = self.interpreter.get_unit_registry(); let unit_names = registry .inner .iter_base_entries() .chain(registry.inner.iter_derived_entries()); unit_names.map(|unit_name| { let info = registry .inner .get_base_representation_for_name(&unit_name) .unwrap(); (unit_name, info) }) } pub fn resolver(&self) -> &Resolver { &self.resolver } pub fn interpret( &mut self, code: &str, code_source: CodeSource, ) -> Result<(Vec, InterpreterResult)> { self.interpret_with_settings(&mut InterpreterSettings::default(), code, code_source) } pub fn interpret_with_settings( &mut self, settings: &mut InterpreterSettings, code: &str, code_source: CodeSource, ) -> Result<(Vec, InterpreterResult)> { let statements = self .resolver .resolve(code, code_source.clone()) .map_err(NumbatError::ResolverError)?; let prefix_transformer_old = self.prefix_transformer.clone(); let result = self .prefix_transformer .transform(statements) .map_err(NumbatError::NameResolutionError); if result.is_err() { // Reset the state of the prefix transformer to what we had before. This is necessary // for REPL use cases where we want to back track from type-check errors. // For example: // // >>> fn f(h) = 1 // error: identifier clash in definition // … // >>> fn f(h_) = 1 # <-- here we want to use 'f' again // self.prefix_transformer = prefix_transformer_old.clone(); } let transformed_statements = result?; let typechecker_old = self.typechecker.clone(); let result = self .typechecker .check_statements(transformed_statements) .map_err(NumbatError::TypeCheckError); if result.is_err() { // Reset the state of the prefix transformer to what we had before. This is necessary // for REPL use cases where we want to back track from type-check errors. // For example: // // >>> let x: Length = 1s # <-- here we register the name 'x' before type checking // Type check error: Incompatible dimensions in variable definition: // specified dimension: Length // actual dimension: Time // >>> let x: Length = 1m # <-- here we want to use the name 'x' again // self.prefix_transformer = prefix_transformer_old.clone(); self.typechecker = typechecker_old.clone(); if self.load_currency_module_on_demand { if let Err(NumbatError::TypeCheckError(TypeCheckError::UnknownIdentifier( _, identifier, _, ))) = &result { // TODO: maybe we can somehow load this list of identifiers from units::currencies? const CURRENCY_IDENTIFIERS: &[&str] = &[ "$", "USD", "dollar", "dollars", "A$", "AUD", "australian_dollar", "australian_dollars", "C$", "CAD", "canadian_dollar", "canadian_dollars", "CHF", "swiss_franc", "swiss_francs", "CNY", "renminbi", "元", "EUR", "euro", "euros", "€", "GBP", "british_pound", "pound_sterling", "£", "JPY", "yen", "yens", "¥", "円", "bulgarian_lev", "bulgarian_leva", "BGN", "czech_koruna", "czech_korunas", "CZK", "Kč", "hungarian_forint", "hungarian_forints", "HUF", "Ft", "polish_zloty", "polish_zlotys", "PLN", "zł", "romanian_leu", "romanian_leus", "RON", "lei", "turkish_lira", "turkish_liras", "TRY", "₺", "brazilian_real", "brazilian_reals", "BRL", "R$", "hong_kong_dollar", "hong_kong_dollars", "HKD", "HK$", "indonesian_rupiah", "indonesian_rupiahs", "IDR", "Rp", "indian_rupee", "indian_rupees", "INR", "₹", "south_korean_won", "south_korean_wons", "KRW", "₩", "malaysian_ringgit", "malaysian_ringgits", "MYR", "RM", "new_zealand_dollar", "new_zealand_dollars", "NZD", "NZ$", "philippine_peso", "philippine_pesos", "PHP", "₱", "singapore_dollar", "singapore_dollars", "SGD", "S$", "thai_baht", "thai_bahts", "THB", "฿", "danish_krone", "danish_kroner", "DKK", "swedish_krona", "swedish_kronor", "SEK", "icelandic_króna", "icelandic_krónur", "ISK", "norwegian_krone", "norwegian_kroner", "NOK", "israeli_new_shekel", "israeli_new_shekels", "ILS", "₪", "NIS", "south_african_rand", "ZAR", ]; if CURRENCY_IDENTIFIERS.contains(&identifier.as_str()) { let mut no_print_settings = InterpreterSettings { print_fn: Box::new( move |_: &m::Markup| { // ignore any print statements when loading this module asynchronously }, ), }; // We also call this from a thread at program startup, so if a user only starts // to use currencies later on, this will already be available and return immediately. // Otherwise, we fetch it now and make sure to block on this call. { let erc = ExchangeRatesCache::fetch(); if erc.is_none() { return Err(NumbatError::RuntimeError( RuntimeError::CouldNotLoadExchangeRates, )); } } let _ = self.interpret_with_settings( &mut no_print_settings, "use units::currencies", CodeSource::Internal, )?; // Make sure we do not run into an infinite loop in case loading that // module did not bring in the required currency unit identifier. This // can happen if the list of currency identifiers is not in sync with // what the module actually defines. self.load_currency_module_on_demand = false; // Now we try to evaluate the user expression again: return self.interpret_with_settings(settings, code, code_source); } } } } let typed_statements = result?; let result = self .interpreter .interpret_statements(settings, &typed_statements); if result.is_err() { // Similar to above: we need to reset the state of the typechecker and the prefix transformer // here for REPL use cases like: // // >>> let q = 1 / 0 // error: runtime error // = Division by zero // // -> 'q' should not be defined, so 'q' properly leads to a "unknown identifier" error // and another 'let q = …' works as intended. // self.prefix_transformer = prefix_transformer_old; self.typechecker = typechecker_old; } let result = result.map_err(NumbatError::RuntimeError)?; Ok((typed_statements, result)) } pub fn print_diagnostic(&self, error: impl ErrorDiagnostic) { use codespan_reporting::term::{ self, termcolor::{ColorChoice, StandardStream}, Config, }; let writer = StandardStream::stderr(ColorChoice::Auto); let config = Config::default(); // we want to be sure no one can write between our diagnostics let mut writer = writer.lock(); for diagnostic in error.diagnostics() { term::emit(&mut writer, &config, &self.resolver.files, &diagnostic).unwrap(); } } pub fn set_terminal_width(&mut self, width: Option) { self.terminal_width = width; } } numbat-1.11.0/src/markup.rs000064400000000000000000000111521046102023000136260ustar 00000000000000use std::fmt::Display; #[derive(Debug, Copy, Clone, PartialEq)] pub enum FormatType { Whitespace, Emphasized, Dimmed, Text, String, Keyword, Value, Unit, Identifier, TypeIdentifier, Operator, Decorator, } #[derive(Debug, Clone, PartialEq)] pub enum OutputType { Normal, Optional, } #[derive(Debug, Clone, PartialEq)] pub struct FormattedString(pub OutputType, pub FormatType, pub String); #[derive(Debug, Clone, Default, PartialEq)] pub struct Markup(pub Vec); impl Markup { pub fn from(f: FormattedString) -> Self { Self(vec![f]) } } impl Display for Markup { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", PlainTextFormatter {}.format(self, false)) } } impl std::ops::Add for Markup { type Output = Markup; fn add(self, rhs: Self) -> Self::Output { let mut res = self.0; res.extend_from_slice(&rhs.0); Markup(res) } } impl std::ops::AddAssign for Markup { fn add_assign(&mut self, rhs: Self) { self.0.extend(rhs.0) } } impl std::iter::Sum for Markup { fn sum>(iter: I) -> Self { iter.fold(empty(), |acc, n| acc + n) } } pub fn space() -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Whitespace, " ".to_string(), )) } pub fn empty() -> Markup { Markup::default() } pub fn whitespace(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Whitespace, text.as_ref().to_string(), )) } pub fn emphasized(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Emphasized, text.as_ref().to_string(), )) } pub fn dimmed(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Dimmed, text.as_ref().to_string(), )) } pub fn text(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Text, text.as_ref().to_string(), )) } pub fn string(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::String, text.as_ref().to_string(), )) } pub fn keyword(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Keyword, text.as_ref().to_string(), )) } pub fn value(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Value, text.as_ref().to_string(), )) } pub fn unit(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Unit, text.as_ref().to_string(), )) } pub fn identifier(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Identifier, text.as_ref().to_string(), )) } pub fn type_identifier(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::TypeIdentifier, text.as_ref().to_string(), )) } pub fn operator(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Operator, text.as_ref().to_string(), )) } pub fn decorator(text: impl AsRef) -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Decorator, text.as_ref().to_string(), )) } pub fn nl() -> Markup { Markup::from(FormattedString( OutputType::Normal, FormatType::Whitespace, "\n".into(), )) } pub trait Formatter { fn format_part(&self, part: &FormattedString) -> String; fn format(&self, markup: &Markup, indent: bool) -> String { let spaces = self.format_part(&FormattedString( OutputType::Normal, FormatType::Whitespace, " ".into(), )); let mut output: String = String::new(); if indent { output.push_str(&spaces); } for part in &markup.0 { output.push_str(&self.format_part(part)); if indent && part.2.contains('\n') { output.push_str(&spaces); } } output } } pub struct PlainTextFormatter; impl Formatter for PlainTextFormatter { fn format_part(&self, FormattedString(_, _, text): &FormattedString) -> String { text.clone() } } numbat-1.11.0/src/math.rs000064400000000000000000000006011046102023000132550ustar 00000000000000/// Calculates the factorial of (the floor of) the given `f64`. /// /// It is the caller's responsibility to ensure that the given `f64` is a /// non-negative integer. pub fn factorial(mut x: f64) -> f64 { debug_assert!(x >= 0.0); x = x.floor(); let mut result = 1f64; while x >= 1. && result != f64::INFINITY { result *= x; x -= 1.; } result } numbat-1.11.0/src/module_importer.rs000064400000000000000000000106171046102023000155420ustar 00000000000000use std::{ ffi::OsStr, fs, path::{Path, PathBuf}, }; use rust_embed::RustEmbed; use crate::resolver::ModulePath; pub trait ModuleImporter: Send + Sync { fn import(&self, path: &ModulePath) -> Option<(String, Option)>; fn list_modules(&self) -> Vec; } #[derive(Debug, Clone, Default)] pub struct NullImporter {} impl ModuleImporter for NullImporter { fn import(&self, _: &ModulePath) -> Option<(String, Option)> { None } fn list_modules(&self) -> Vec { vec![] } } #[derive(Debug, Clone, Default)] pub struct FileSystemImporter { root_paths: Vec, } impl FileSystemImporter { pub fn add_path>(&mut self, path: P) { self.root_paths.push(path.as_ref().to_owned()); } } impl ModuleImporter for FileSystemImporter { fn import(&self, module_path: &ModulePath) -> Option<(String, Option)> { for path in &self.root_paths { let mut path = path.clone(); for part in &module_path.0 { path = path.join(part); } path.set_extension("nbt"); if let Ok(code) = fs::read_to_string(&path) { return Some((code, Some(path.to_owned()))); } } None } fn list_modules(&self) -> Vec { use walkdir::WalkDir; let mut modules = vec![]; for root_path in &self.root_paths { for entry in WalkDir::new(root_path) .follow_links(true) .follow_root_links(false) .into_iter() .flatten() { let path = entry.path(); if path.is_file() && path.extension() == Some(OsStr::new("nbt")) { if let Ok(relative_path) = path.strip_prefix(root_path) { let components: Vec = relative_path .components() .map(|c| { c.as_os_str() .to_string_lossy() .trim_end_matches(".nbt") .to_string() }) .collect(); modules.push(ModulePath(components)); } } } } modules } } #[derive(RustEmbed)] #[folder = "$CARGO_MANIFEST_DIR/modules/"] struct BuiltinAssets; #[derive(Debug, Clone, Default)] pub struct BuiltinModuleImporter {} impl ModuleImporter for BuiltinModuleImporter { fn import(&self, module_path: &ModulePath) -> Option<(String, Option)> { let mut path = PathBuf::new(); for part in &module_path.0 { path = path.join(part); } path.set_extension("nbt"); BuiltinAssets::get(&path.to_string_lossy()) .map(|embedded_file| { let content = embedded_file.data.into_owned(); String::from_utf8(content).expect("Numbat modules are properly UTF-8 encoded") }) .map(|content| { let user_facing_path = PathBuf::from("").join("modules").join(path); (content, Some(user_facing_path)) }) } fn list_modules(&self) -> Vec { BuiltinAssets::iter() .map(|path| { ModulePath( path.trim_end_matches(".nbt") .split('/') .map(|s| s.to_string()) .collect(), ) }) .collect() } } pub struct ChainedImporter { main: Box, fallback: Box, } impl ChainedImporter { pub fn new(main: Box, fallback: Box) -> Self { Self { main, fallback } } } impl ModuleImporter for ChainedImporter { fn import(&self, path: &ModulePath) -> Option<(String, Option)> { if let result @ Some(_) = self.main.import(path) { result } else { self.fallback.import(path) } } fn list_modules(&self) -> Vec { let mut modules = self.main.list_modules(); modules.extend(self.fallback.list_modules()); modules.sort(); modules.dedup(); modules } } numbat-1.11.0/src/name_resolution.rs000064400000000000000000000006701046102023000155350ustar 00000000000000use thiserror::Error; use crate::span::Span; pub const LAST_RESULT_IDENTIFIERS: &[&str] = &["ans", "_"]; #[derive(Debug, Clone, Error)] pub enum NameResolutionError { #[error("Identifier is already in use: '{conflicting_identifier}'.")] IdentifierClash { conflicting_identifier: String, conflict_span: Span, original_span: Span, }, #[error("Reserved identifier")] ReservedIdentifier(Span), } numbat-1.11.0/src/number.rs000064400000000000000000000112211046102023000136140ustar 00000000000000use num_traits::{Pow, ToPrimitive}; #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] // TODO: we probably want to remove 'Copy' once we move to a more sophisticated numerical type pub struct Number(pub f64); impl Eq for Number {} impl Number { pub fn from_f64(n: f64) -> Self { Number(n) } pub fn to_f64(self) -> f64 { let Number(n) = self; n } pub fn pow(self, other: &Number) -> Self { Number::from_f64(self.to_f64().pow(other.to_f64())) } fn is_integer(self) -> bool { self.0.trunc() == self.0 } pub fn pretty_print(self) -> String { let number = self.0; // 64-bit floats can accurately represent integers up to 2^52 [1], // which is approximately 4.5 × 10^15. // // [1] https://stackoverflow.com/a/43656339 // if self.is_integer() && self.0.abs() < 1e15 { use num_format::{CustomFormat, Grouping, ToFormattedString}; let format = CustomFormat::builder() .grouping(if self.0.abs() >= 100_000.0 { Grouping::Standard } else { Grouping::Posix }) .minus_sign("-") .separator("_") .build() .unwrap(); number .to_i64() .expect("small enough integers are representable as i64") .to_formatted_string(&format) } else { use pretty_dtoa::{dtoa, FmtFloatConfig}; let config = FmtFloatConfig::default() .max_significant_digits(6) .add_point_zero(false) .lower_e_break(-6) .upper_e_break(6) .round(); let formatted_number = dtoa(number, config); if formatted_number.contains('.') && !formatted_number.contains('e') { let formatted_number = formatted_number.trim_end_matches('0'); if formatted_number.ends_with('.') { format!("{}0", formatted_number) } else { formatted_number.to_string() } } else if formatted_number.contains('e') && !formatted_number.contains("e-") { formatted_number.replace('e', "e+") } else { formatted_number } } } } impl std::ops::Add for Number { type Output = Number; fn add(self, rhs: Self) -> Self::Output { Number(self.0 + rhs.0) } } impl std::ops::Sub for Number { type Output = Number; fn sub(self, rhs: Self) -> Self::Output { Number(self.0 - rhs.0) } } impl std::ops::Mul for Number { type Output = Number; fn mul(self, rhs: Self) -> Self::Output { Number(self.0 * rhs.0) } } impl std::ops::Div for Number { type Output = Number; fn div(self, rhs: Self) -> Self::Output { Number(self.0 / rhs.0) } } impl std::ops::Neg for Number { type Output = Number; fn neg(self) -> Self::Output { Number(-self.0) } } impl std::iter::Product for Number { fn product>(iter: I) -> Self { iter.fold(Number::from_f64(1.0), |acc, n| acc * n) } } #[test] fn test_pretty_print() { assert_eq!(Number::from_f64(1.).pretty_print(), "1"); assert_eq!(Number::from_f64(100.).pretty_print(), "100"); assert_eq!(Number::from_f64(1.234).pretty_print(), "1.234"); assert_eq!(Number::from_f64(12345.6).pretty_print(), "12345.6"); assert_eq!(Number::from_f64(1.234e50).pretty_print(), "1.234e+50"); assert_eq!(Number::from_f64(-1.234e50).pretty_print(), "-1.234e+50"); assert_eq!(Number::from_f64(1.234e-50).pretty_print(), "1.234e-50"); assert_eq!(Number::from_f64(-1.234e-50).pretty_print(), "-1.234e-50"); assert_eq!(Number::from_f64(1234.).pretty_print(), "1234"); assert_eq!(Number::from_f64(12345.).pretty_print(), "12345"); assert_eq!(Number::from_f64(123456.).pretty_print(), "123_456"); assert_eq!( Number::from_f64(1234567890.).pretty_print(), "1_234_567_890" ); assert_eq!( Number::from_f64(1234567890000000.).pretty_print(), "1.23457e+15" ); assert_eq!(Number::from_f64(1.23456789).pretty_print(), "1.23457"); assert_eq!( Number::from_f64(1234567890000.1).pretty_print(), "1.23457e+12" ); assert_eq!(Number::from_f64(100.00001).pretty_print(), "100.0"); assert_eq!(Number::from_f64(0.00001).pretty_print(), "0.00001"); assert_eq!(Number::from_f64(0.000001).pretty_print(), "0.000001"); assert_eq!(Number::from_f64(0.0000001).pretty_print(), "1.0e-7"); } numbat-1.11.0/src/parser.rs000064400000000000000000002762251046102023000136410ustar 00000000000000//! Numbat Parser //! //! Grammar: //! ```txt //! statement ::= variable_decl | function_decl | dimension_decl | unit_decl | module_import | procedure_call | expression //! //! variable_decl ::= "let" identifier ( ":" type_annotation ) ? "=" expression //! function_decl ::= "fn" identifier ( fn_decl_generic ) ? fn_decl_param ( "->" type_annotation ) ? ( "=" expression ) ? //! fn_decl_generic ::= "<" ( identifier "," ) * identifier ">" //! fn_decl_param ::= "(" ( identifier ( ":" type_annotation ) ? "," )* ( identifier ( ":" type_annotation ) ) ? ")" //! dimension_decl ::= "dimension" identifier ( "=" dimension_expr ) * //! unit_decl ::= decorator * "unit" ( ":" dimension_expr ) ? ( "=" expression ) ? //! module_import ::= "use" ident ( "::" ident) * //! procedure_call ::= ( "print" | "assert" | "assert_eq" | "type" ) "(" arguments? ")" //! //! decorator ::= "@" ( "metric_prefixes" | "binary_prefixes" | ( "aliases(" list_of_aliases ")" ) ) //! //! type_annotation ::= "Bool" | "String" | dimension_expr //! dimension_expr ::= dim_factor //! dim_factor ::= dim_power ( (multiply | divide) dim_power ) * //! dim_power ::= dim_primary ( power dim_exponent | unicode_exponent ) ? //! dim_exponent ::= integer | minus dim_exponent | "(" dim_exponent ( divide dim_exponent ) ? ")" //! dim_primary ::= identifier | "1" | "(" dimension_expr ")" //! //! expression ::= postfix_apply //! postfix_apply ::= condition ( "//" identifier ) * //! condition ::= ( "if" conversion "then" condition "else" condition ) | conversion //! conversion ::= logical_or ( ( "→" | "->" | "to" ) logical_or ) * //! logical_or ::= logical_and ( "||" logical_and ) * //! logical_and ::= logical_neg ( "&&" logical_neg ) * //! logical_neg ::= ( "!" logical_neg) | comparison //! comparison ::= term ( (">" | ">="| "≥" | "<" | "<=" | "≤" | "==" | "!=" | "≠" ) term ) * //! term ::= factor ( ( "+" | "-") factor ) * //! factor ::= unary ( ( "*" | "/") per_factor ) * //! per_factor ::= unary ( "per" unary ) * //! unary ::= ( ( minus | plus ) unary ) | ifactor //! ifactor ::= power ( " " power ) * //! power ::= factorial ( "^" "-" ? power ) ? //! factorial ::= unicode_power "!" * //! unicode_power ::= call ( "⁻" ? ( "¹" | "²" | "³" | "⁴" | "⁵" | "⁶" | "⁷" | "⁸" | "⁹" ) ) ? //! call ::= primary ( "(" arguments? ")" ) * //! arguments ::= expression ( "," expression ) * //! primary ::= boolean | string | hex_number | oct_number | bin_number | number | identifier | "(" expression ")" //! //! number ::= [0-9][0-9_]*("." ([0-9][0-9_]*)?)?([eE][+-]?[0-9][0-9_]*)? //! hex_number ::= "0x" [0-9a-fA-F]* //! oct_number ::= "0o" [0-7]* //! bin_number ::= "0b" [01]* //! integer ::= [0-9]([0-9_]*[0-9])? //! identifier ::= identifier_s identifier_c* //! identifier_s ::= Unicode_XID_Start | Unicode_Currency | "%" | "°" | "_" //! identifier_c ::= Unicode_XID_Continue | Unicode_Currency | "%" //! boolean ::= "true" | "false" //! plus ::= "+" //! minus ::= "-" //! multiply ::= "*" | "×" | "·" //! divide ::= "/" | "÷" //! string ::= '"' [^"]* '"' //! ``` use crate::arithmetic::{Exponent, Rational}; use crate::ast::{ BinaryOperator, DimensionExpression, Expression, ProcedureKind, Statement, StringPart, TypeAnnotation, UnaryOperator, }; use crate::decorator::{self, Decorator}; use crate::number::Number; use crate::prefix_parser::AcceptsPrefix; use crate::resolver::ModulePath; use crate::span::Span; use crate::tokenizer::{Token, TokenKind, TokenizerError, TokenizerErrorKind}; use num_traits::{CheckedDiv, FromPrimitive, Zero}; use thiserror::Error; #[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum ParseErrorKind { #[error("{0}")] TokenizerError(TokenizerErrorKind), #[error("Expected one of: number, identifier, parenthesized expression")] ExpectedPrimary, #[error("Missing closing parenthesis ')'")] MissingClosingParen, #[error("Trailing characters: '{0}'")] TrailingCharacters(String), #[error("Trailing '=' sign. Use `let {0} = …` if you intended to define a new constant.")] TrailingEqualSign(String), #[error("Expected identifier after 'let' keyword")] ExpectedIdentifierAfterLet, #[error("Expected '=' or ':' after identifier (and type annotation) in 'let' assignment")] ExpectedEqualOrColonAfterLetIdentifier, #[error("Expected identifier after 'fn' keyword. Note that some reserved words can not be used as function names.")] ExpectedIdentifierAfterFn, #[error("Expected identifier")] ExpectedIdentifier, #[error("Expected dimension identifier, '1', or opening parenthesis")] ExpectedDimensionPrimary, #[error("Expected ',' or '>' in type parameter list")] ExpectedCommaOrRightAngleBracket, #[error("Expected identifier (type parameter name)")] ExpectedTypeParameterName, #[error("Expected opening parenthesis '(' in function definition")] ExpectedLeftParenInFunctionDefinition, #[error("Expected ',', '…', or ')' in function parameter list")] ExpectedCommaEllipsisOrRightParenInFunctionDefinition, #[error("Expected parameter name in function definition")] ExpectedParameterNameInFunctionDefinition, #[error("Only a single variadic parameter is allowed in a function definition")] OnlySingleVariadicParameter, #[error("Variadic parameters are only allowed in foreign functions (without body)")] VariadicParameterOnlyAllowedInForeignFunction, #[error("Expected identifier (dimension name)")] ExpectedIdentifierAfterDimension, #[error("Expected identifier (unit name)")] ExpectedIdentifierAfterUnit, #[error("Expected '=' or ':' after identifier in unit definition")] ExpectedColonOrEqualAfterUnitIdentifier, #[error("Only functions can be called")] CanOnlyCallIdentifier, #[error("Division by zero in dimension exponent")] DivisionByZeroInDimensionExponent, #[error("Expected opening parenthesis '(' after procedure name")] ExpectedLeftParenAfterProcedureName, #[error("Procedures can not be used inside an expression")] InlineProcedureUsage, #[error("Expected decorator name")] ExpectedDecoratorName, #[error("Unknown decorator name")] UnknownDecorator, #[error("Expected module path after 'use'")] ExpectedModulePathAfterUse, #[error("Expected module name after double colon (::)")] ExpectedModuleNameAfterDoubleColon, #[error("Overflow in number literal")] OverflowInNumberLiteral, #[error("Expected dimension exponent")] ExpectedDimensionExponent, #[error("Double-underscore type names are reserved for internal use")] DoubleUnderscoreTypeNamesReserved, #[error("Only integer numbers (< 2^128) are allowed in dimension exponents")] NumberInDimensionExponentOutOfRange, #[error("Decorators can only be used on unit definitions or let definitions")] DecoratorsCanOnlyBeUsedOnUnitOrLetDefinitions, #[error("Decorators on let definitions cannot have prefix information")] DecoratorsWithPrefixOnLetDefinition, #[error("Expected opening parenthesis after decorator")] ExpectedLeftParenAfterDecorator, #[error("Unknown alias annotation")] UnknownAliasAnnotation, #[error("Numerical overflow in dimension exponent")] OverflowInDimensionExponent, #[error("Expected 'then' in if-then-else condition")] ExpectedThen, #[error("Expected 'else' in if-then-else condition")] ExpectedElse, #[error("Unterminated string")] UnterminatedString, #[error("Expected a string")] ExpectedString, #[error("Expected {0} in function type")] ExpectedTokenInFunctionType(&'static str), } #[derive(Debug, Clone, Error)] #[error("{kind}")] pub struct ParseError { pub kind: ParseErrorKind, pub span: Span, } impl ParseError { fn new(kind: ParseErrorKind, span: Span) -> Self { ParseError { kind, span } } } type Result = std::result::Result; type ParseResult = Result, (Vec, Vec)>; static PROCEDURES: &[TokenKind] = &[ TokenKind::ProcedurePrint, TokenKind::ProcedureAssert, TokenKind::ProcedureAssertEq, TokenKind::ProcedureType, ]; struct Parser<'a> { tokens: &'a [Token], current: usize, decorator_stack: Vec, } impl<'a> Parser<'a> { pub(crate) fn new(tokens: &'a [Token]) -> Self { Parser { tokens, current: 0, decorator_stack: vec![], } } fn skip_empty_lines(&mut self) { while self.match_exact(TokenKind::Newline).is_some() {} } /// Parse a token stream. /// If an error is encountered and `stop_on_error` is set to false, the parser /// will try to recover from the error and parse as many statements as possible /// while stacking all the errors in a `Vec`. At the end, it returns the complete /// list of statements parsed + the list of errors accumulated. fn parse(&mut self) -> ParseResult { let mut statements = vec![]; let mut errors = vec![]; self.skip_empty_lines(); while !self.is_at_end() { match self.statement() { Ok(statement) => statements.push(statement), Err(e) => { errors.push(e); self.recover_from_error(); } } match self.peek().kind { TokenKind::Newline => { // Skip over empty lines while self.match_exact(TokenKind::Newline).is_some() {} } TokenKind::Eof => { break; } TokenKind::Equal => { errors.push(ParseError { kind: ParseErrorKind::TrailingEqualSign( self.last().unwrap().lexeme.clone(), ), span: self.peek().span, }); self.recover_from_error(); } _ => { errors.push(ParseError { kind: ParseErrorKind::TrailingCharacters(self.peek().lexeme.clone()), span: self.peek().span, }); self.recover_from_error(); } } } if errors.is_empty() { Ok(statements) } else { Err((statements, errors)) } } /// Must be called after encountering an error. fn recover_from_error(&mut self) { // Skip all the tokens until we encounter a newline or EoF. while !matches!(self.peek().kind, TokenKind::Newline | TokenKind::Eof) { self.advance() } } fn accepts_prefix(&mut self) -> Result> { if self.match_exact(TokenKind::Colon).is_some() { if self.match_exact(TokenKind::Long).is_some() { Ok(Some(AcceptsPrefix::only_long())) } else if self.match_exact(TokenKind::Short).is_some() { Ok(Some(AcceptsPrefix::only_short())) } else if self.match_exact(TokenKind::Both).is_some() { Ok(Some(AcceptsPrefix::both())) } else if self.match_exact(TokenKind::None).is_some() { Ok(Some(AcceptsPrefix::none())) } else { return Err(ParseError::new( ParseErrorKind::UnknownAliasAnnotation, self.peek().span, )); } } else { Ok(None) } } fn list_of_aliases(&mut self) -> Result)>> { if self.match_exact(TokenKind::RightParen).is_some() { return Ok(vec![]); } let mut identifiers: Vec<(String, Option)> = vec![(self.identifier()?, self.accepts_prefix()?)]; while self.match_exact(TokenKind::Comma).is_some() { identifiers.push((self.identifier()?, self.accepts_prefix()?)); } if self.match_exact(TokenKind::RightParen).is_none() { return Err(ParseError::new( ParseErrorKind::MissingClosingParen, self.peek().span, )); } Ok(identifiers) } fn statement(&mut self) -> Result { if !(self.peek().kind == TokenKind::At || self.peek().kind == TokenKind::Unit || self.peek().kind == TokenKind::Let || self.decorator_stack.is_empty()) { return Err(ParseError { kind: ParseErrorKind::DecoratorsCanOnlyBeUsedOnUnitOrLetDefinitions, span: self.peek().span, }); } if self.match_exact(TokenKind::Let).is_some() { if let Some(identifier) = self.match_exact(TokenKind::Identifier) { let identifier_span = self.last().unwrap().span; let type_annotation = if self.match_exact(TokenKind::Colon).is_some() { Some(self.type_annotation()?) } else { None }; if self.match_exact(TokenKind::Equal).is_none() { Err(ParseError { kind: ParseErrorKind::ExpectedEqualOrColonAfterLetIdentifier, span: self.peek().span, }) } else { self.skip_empty_lines(); let expr = self.expression()?; if decorator::contains_aliases_with_prefixes(&self.decorator_stack) { return Err(ParseError { kind: ParseErrorKind::DecoratorsWithPrefixOnLetDefinition, span: self.peek().span, }); } let mut decorators = vec![]; std::mem::swap(&mut decorators, &mut self.decorator_stack); Ok(Statement::DefineVariable { identifier_span, identifier: identifier.lexeme.clone(), expr, type_annotation, decorators, }) } } else { Err(ParseError { kind: ParseErrorKind::ExpectedIdentifierAfterLet, span: self.peek().span, }) } } else if self.match_exact(TokenKind::Fn).is_some() { if let Some(fn_name) = self.match_exact(TokenKind::Identifier) { let function_name_span = self.last().unwrap().span; let mut type_parameters = vec![]; // Parsing the generic parameters if there are any if self.match_exact(TokenKind::LessThan).is_some() { while self.match_exact(TokenKind::GreaterThan).is_none() { if let Some(type_parameter_name) = self.match_exact(TokenKind::Identifier) { let span = self.last().unwrap().span; type_parameters.push((span, type_parameter_name.lexeme.to_string())); if self.match_exact(TokenKind::Comma).is_none() && self.peek().kind != TokenKind::GreaterThan { return Err(ParseError { kind: ParseErrorKind::ExpectedCommaOrRightAngleBracket, span: self.peek().span, }); } } else { return Err(ParseError { kind: ParseErrorKind::ExpectedTypeParameterName, span: self.peek().span, }); } } } if self.match_exact(TokenKind::LeftParen).is_none() { return Err(ParseError { kind: ParseErrorKind::ExpectedLeftParenInFunctionDefinition, span: self.peek().span, }); } let mut parameter_span = self.peek().span; let mut parameters = vec![]; while self.match_exact(TokenKind::RightParen).is_none() { if let Some(param_name) = self.match_exact(TokenKind::Identifier) { let span = self.last().unwrap().span; let param_type_dexpr = if self.match_exact(TokenKind::Colon).is_some() { Some(self.type_annotation()?) } else { None }; let is_variadic = self.match_exact(TokenKind::Ellipsis).is_some(); parameters.push(( span, param_name.lexeme.to_string(), param_type_dexpr, is_variadic, )); parameter_span = parameter_span.extend(&self.last().unwrap().span); let mut has_comma = || -> bool { let yes = self.match_exact(TokenKind::Comma).is_some(); self.match_exact(TokenKind::Newline); yes }; if !has_comma() && self.peek().kind != TokenKind::RightParen { return Err(ParseError { kind: ParseErrorKind::ExpectedCommaEllipsisOrRightParenInFunctionDefinition, span: self.peek().span, }); } } else { return Err(ParseError { kind: ParseErrorKind::ExpectedParameterNameInFunctionDefinition, span: self.peek().span, }); } } let (return_type_span, return_type_annotation) = if self.match_exact(TokenKind::Arrow).is_some() { let return_type_annotation = self.type_annotation()?; ( Some(self.last().unwrap().span), Some(return_type_annotation), ) } else { (None, None) }; let fn_is_variadic = parameters.iter().any(|p| p.3); if fn_is_variadic && parameters.len() > 1 { return Err(ParseError { kind: ParseErrorKind::OnlySingleVariadicParameter, span: parameter_span, }); } let body = if self.match_exact(TokenKind::Equal).is_none() { None } else { self.skip_empty_lines(); Some(self.expression()?) }; if fn_is_variadic && body.is_some() { return Err(ParseError { kind: ParseErrorKind::VariadicParameterOnlyAllowedInForeignFunction, span: parameter_span, }); } Ok(Statement::DefineFunction { function_name_span, function_name: fn_name.lexeme.clone(), type_parameters, parameters, body, return_type_annotation_span: return_type_span, return_type_annotation, }) } else { Err(ParseError { kind: ParseErrorKind::ExpectedIdentifierAfterFn, span: self.peek().span, }) } } else if self.match_exact(TokenKind::Dimension).is_some() { if let Some(identifier) = self.match_exact(TokenKind::Identifier) { if identifier.lexeme.starts_with("__") { return Err(ParseError::new( ParseErrorKind::DoubleUnderscoreTypeNamesReserved, identifier.span, )); } if self.match_exact(TokenKind::Equal).is_some() { self.skip_empty_lines(); let mut dexprs = vec![self.dimension_expression()?]; while self.match_exact(TokenKind::Equal).is_some() { self.skip_empty_lines(); dexprs.push(self.dimension_expression()?); } Ok(Statement::DefineDimension( identifier.lexeme.clone(), dexprs, )) } else { Ok(Statement::DefineDimension( identifier.lexeme.clone(), vec![], )) } } else { Err(ParseError { kind: ParseErrorKind::ExpectedIdentifierAfterDimension, span: self.peek().span, }) } } else if self.match_exact(TokenKind::At).is_some() { if let Some(decorator) = self.match_exact(TokenKind::Identifier) { let decorator = if decorator.lexeme == "metric_prefixes" { Decorator::MetricPrefixes } else if decorator.lexeme == "binary_prefixes" { Decorator::BinaryPrefixes } else if decorator.lexeme == "aliases" { if self.match_exact(TokenKind::LeftParen).is_some() { let aliases = self.list_of_aliases()?; Decorator::Aliases(aliases) } else { return Err(ParseError { kind: ParseErrorKind::ExpectedLeftParenAfterDecorator, span: self.peek().span, }); } } else if decorator.lexeme == "url" || decorator.lexeme == "name" { if self.match_exact(TokenKind::LeftParen).is_some() { if let Some(token) = self.match_exact(TokenKind::StringFixed) { if self.match_exact(TokenKind::RightParen).is_none() { return Err(ParseError::new( ParseErrorKind::MissingClosingParen, self.peek().span, )); } let content = token.lexeme.trim_matches('"'); match decorator.lexeme.as_str() { "url" => Decorator::Url(content.into()), "name" => Decorator::Name(content.into()), _ => unreachable!(), } } else { return Err(ParseError { kind: ParseErrorKind::ExpectedString, span: self.peek().span, }); } } else { return Err(ParseError { kind: ParseErrorKind::ExpectedLeftParenAfterDecorator, span: self.peek().span, }); } } else { return Err(ParseError { kind: ParseErrorKind::UnknownDecorator, span: decorator.span, }); }; self.decorator_stack.push(decorator); // TODO: make sure that there are no duplicate decorators // A decorator is not yet a full statement. Continue parsing: self.skip_empty_lines(); self.statement() } else { Err(ParseError { kind: ParseErrorKind::ExpectedDecoratorName, span: self.peek().span, }) } } else if self.match_exact(TokenKind::Unit).is_some() { if let Some(identifier) = self.match_exact(TokenKind::Identifier) { let identifier_span = self.last().unwrap().span; let (type_annotation_span, dexpr) = if self.match_exact(TokenKind::Colon).is_some() { let type_annotation = self.dimension_expression()?; (Some(self.last().unwrap().span), Some(type_annotation)) } else { (None, None) }; let unit_name = identifier.lexeme.clone(); let mut decorators = vec![]; std::mem::swap(&mut decorators, &mut self.decorator_stack); if self.match_exact(TokenKind::Equal).is_some() { self.skip_empty_lines(); let expr = self.expression()?; Ok(Statement::DefineDerivedUnit { identifier_span, identifier: unit_name, expr, type_annotation_span, type_annotation: dexpr, decorators, }) } else if let Some(dexpr) = dexpr { Ok(Statement::DefineBaseUnit( identifier_span, unit_name, Some(dexpr), decorators, )) } else if self.is_end_of_statement() { Ok(Statement::DefineBaseUnit( identifier_span, unit_name, None, decorators, )) } else { Err(ParseError { kind: ParseErrorKind::ExpectedColonOrEqualAfterUnitIdentifier, span: self.peek().span, }) } } else { Err(ParseError { kind: ParseErrorKind::ExpectedIdentifierAfterUnit, span: self.peek().span, }) } } else if self.match_exact(TokenKind::Use).is_some() { let mut span = self.peek().span; if let Some(identifier) = self.match_exact(TokenKind::Identifier) { let mut module_path = vec![identifier.lexeme.clone()]; while self.match_exact(TokenKind::DoubleColon).is_some() { if let Some(identifier) = self.match_exact(TokenKind::Identifier) { module_path.push(identifier.lexeme.clone()); } else { return Err(ParseError { kind: ParseErrorKind::ExpectedModuleNameAfterDoubleColon, span: self.peek().span, }); } } span = span.extend(&self.last().unwrap().span); Ok(Statement::ModuleImport(span, ModulePath(module_path))) } else { Err(ParseError { kind: ParseErrorKind::ExpectedModulePathAfterUse, span: self.peek().span, }) } } else if self.match_any(PROCEDURES).is_some() { let span = self.last().unwrap().span; let procedure_kind = match self.last().unwrap().kind { TokenKind::ProcedurePrint => ProcedureKind::Print, TokenKind::ProcedureAssert => ProcedureKind::Assert, TokenKind::ProcedureAssertEq => ProcedureKind::AssertEq, TokenKind::ProcedureType => ProcedureKind::Type, _ => unreachable!(), }; if self.match_exact(TokenKind::LeftParen).is_none() { Err(ParseError { kind: ParseErrorKind::ExpectedLeftParenAfterProcedureName, span: self.peek().span, }) } else { Ok(Statement::ProcedureCall( span, procedure_kind, self.arguments()?, )) } } else { Ok(Statement::Expression(self.expression()?)) } } /// Helper function to parse binary operations /// - arg `op_symbol` specifiy the separator / symbol of your operation /// - arg `op` specifiy the operation you're currently parsing /// - arg `next` specifiy the next parser to call between each symbols fn parse_binop( &mut self, op_symbol: &[TokenKind], op: impl Fn(TokenKind) -> BinaryOperator, next_parser: impl Fn(&mut Self) -> Result, ) -> Result { let mut expr = next_parser(self)?; while let Some(matched) = self.match_any(op_symbol) { let span_op = Some(self.last().unwrap().span); let rhs = next_parser(self)?; expr = Expression::BinaryOperator { op: op(matched.kind), lhs: Box::new(expr), rhs: Box::new(rhs), span_op, }; } Ok(expr) } pub fn expression(&mut self) -> Result { self.postfix_apply() } fn identifier(&mut self) -> Result { if let Some(identifier) = self.match_exact(TokenKind::Identifier) { Ok(identifier.lexeme.clone()) } else { Err(ParseError::new( ParseErrorKind::ExpectedIdentifier, self.peek().span, )) } } pub fn postfix_apply(&mut self) -> Result { let mut expr = self.condition()?; let mut full_span = expr.full_span(); while self.match_exact(TokenKind::PostfixApply).is_some() { let identifier = self.identifier()?; let identifier_span = self.last().unwrap().span; full_span = full_span.extend(&identifier_span); expr = Expression::FunctionCall( identifier_span, full_span, Box::new(Expression::Identifier(identifier_span, identifier)), vec![expr], ); } Ok(expr) } fn condition(&mut self) -> Result { if self.match_exact(TokenKind::If).is_some() { let span_if = self.last().unwrap().span; let condition_expr = self.conversion()?; self.match_exact(TokenKind::Newline); if self.match_exact(TokenKind::Then).is_none() { return Err(ParseError::new( ParseErrorKind::ExpectedThen, self.peek().span, )); } let then_expr = self.condition()?; self.match_exact(TokenKind::Newline); if self.match_exact(TokenKind::Else).is_none() { return Err(ParseError::new( ParseErrorKind::ExpectedElse, self.peek().span, )); } let else_expr = self.condition()?; Ok(Expression::Condition( span_if, Box::new(condition_expr), Box::new(then_expr), Box::new(else_expr), )) } else { self.conversion() } } fn conversion(&mut self) -> Result { self.parse_binop( &[TokenKind::Arrow, TokenKind::To], |_| BinaryOperator::ConvertTo, Self::logical_or, ) } fn logical_or(&mut self) -> Result { self.parse_binop( &[TokenKind::LogicalOr], |_| BinaryOperator::LogicalOr, Self::logical_and, ) } fn logical_and(&mut self) -> Result { self.parse_binop( &[TokenKind::LogicalAnd], |_| BinaryOperator::LogicalAnd, Self::logical_neg, ) } fn logical_neg(&mut self) -> Result { if self.match_exact(TokenKind::ExclamationMark).is_some() { let span = self.last().unwrap().span; let rhs = self.logical_neg()?; Ok(Expression::UnaryOperator { op: UnaryOperator::LogicalNeg, expr: Box::new(rhs), span_op: span, }) } else { self.comparison() } } fn comparison(&mut self) -> Result { self.parse_binop( &[ TokenKind::LessThan, TokenKind::GreaterThan, TokenKind::LessOrEqual, TokenKind::GreaterOrEqual, TokenKind::EqualEqual, TokenKind::NotEqual, ], |matched| match matched { TokenKind::LessThan => BinaryOperator::LessThan, TokenKind::GreaterThan => BinaryOperator::GreaterThan, TokenKind::LessOrEqual => BinaryOperator::LessOrEqual, TokenKind::GreaterOrEqual => BinaryOperator::GreaterOrEqual, TokenKind::EqualEqual => BinaryOperator::Equal, TokenKind::NotEqual => BinaryOperator::NotEqual, _ => unreachable!(), }, Self::term, ) } fn term(&mut self) -> Result { self.parse_binop( &[TokenKind::Plus, TokenKind::Minus], |matched| match matched { TokenKind::Plus => BinaryOperator::Add, TokenKind::Minus => BinaryOperator::Sub, _ => unreachable!(), }, Self::factor, ) } fn factor(&mut self) -> Result { self.parse_binop( &[TokenKind::Multiply, TokenKind::Divide], |matched| match matched { TokenKind::Multiply => BinaryOperator::Mul, TokenKind::Divide => BinaryOperator::Div, _ => unreachable!(), }, Self::per_factor, ) } fn per_factor(&mut self) -> Result { self.parse_binop(&[TokenKind::Per], |_| BinaryOperator::Div, Self::unary) } fn unary(&mut self) -> Result { if self.match_exact(TokenKind::Minus).is_some() { let span = self.last().unwrap().span; let rhs = self.unary()?; Ok(Expression::UnaryOperator { op: UnaryOperator::Negate, expr: Box::new(rhs), span_op: span, }) } else if self.match_exact(TokenKind::Plus).is_some() { // A unary `+` is equivalent to nothing. We can get rid of the // symbol without inserting any nodes in the AST. self.unary() } else { self.ifactor() } } fn ifactor(&mut self) -> Result { let mut expr = self.power()?; while self.next_token_could_start_power_expression() { let rhs = self.power()?; expr = Expression::BinaryOperator { op: BinaryOperator::Mul, lhs: Box::new(expr), rhs: Box::new(rhs), span_op: None, }; } Ok(expr) } fn power(&mut self) -> Result { let mut expr = self.factorial()?; if self.match_exact(TokenKind::Power).is_some() { let span_op = Some(self.last().unwrap().span); let unary_op = if self.match_exact(TokenKind::Minus).is_some() { let span_unary_minus = self.last().unwrap().span; Some((UnaryOperator::Negate, span_unary_minus)) } else { None }; let mut rhs = self.power()?; if let Some((op, span_op)) = unary_op { rhs = Expression::UnaryOperator { op, expr: Box::new(rhs), span_op, }; } expr = Expression::BinaryOperator { op: BinaryOperator::Power, lhs: Box::new(expr), rhs: Box::new(rhs), span_op, }; } Ok(expr) } fn factorial(&mut self) -> Result { let mut expr = self.unicode_power()?; while self.match_exact(TokenKind::ExclamationMark).is_some() { let span = self.last().unwrap().span; expr = Expression::UnaryOperator { op: UnaryOperator::Factorial, expr: Box::new(expr), span_op: span, }; } Ok(expr) } fn unicode_exponent_to_int(lexeme: &str) -> i32 { match lexeme { "⁻¹" => -1, "⁻²" => -2, "⁻³" => -3, "⁻⁴" => -4, "⁻⁵" => -5, "⁻⁶" => -6, "⁻⁷" => -7, "⁻⁸" => -8, "⁻⁹" => -9, "¹" => 1, "²" => 2, "³" => 3, "⁴" => 4, "⁵" => 5, "⁶" => 6, "⁷" => 7, "⁸" => 8, "⁹" => 9, _ => unreachable!( "Tokenizer should not generate unicode exponent tokens for anything else" ), } } fn unicode_power(&mut self) -> Result { let mut expr = self.call()?; if let Some(exponent) = self.match_exact(TokenKind::UnicodeExponent) { let exp = Self::unicode_exponent_to_int(exponent.lexeme.as_str()); expr = Expression::BinaryOperator { op: BinaryOperator::Power, lhs: Box::new(expr), rhs: Box::new(Expression::Scalar( exponent.span, Number::from_f64(exp as f64), )), span_op: None, }; } Ok(expr) } fn call(&mut self) -> Result { let mut expr = self.primary()?; while self.match_exact(TokenKind::LeftParen).is_some() { let args = self.arguments()?; expr = Expression::FunctionCall( expr.full_span(), expr.full_span().extend(&self.last().unwrap().span), Box::new(expr), args, ); } Ok(expr) } fn arguments(&mut self) -> Result> { if self.match_exact(TokenKind::RightParen).is_some() { return Ok(vec![]); } let mut args: Vec = vec![self.expression()?]; while self.match_exact(TokenKind::Comma).is_some() { args.push(self.expression()?); } if self.match_exact(TokenKind::RightParen).is_none() { return Err(ParseError::new( ParseErrorKind::MissingClosingParen, self.peek().span, )); } Ok(args) } fn primary(&mut self) -> Result { // This function needs to be kept in sync with `next_token_could_start_primary` below. let overflow_error = |span| { Err(ParseError::new( ParseErrorKind::OverflowInNumberLiteral, span, )) }; if let Some(num) = self.match_exact(TokenKind::Number) { let num_string = num.lexeme.replace('_', ""); Ok(Expression::Scalar( self.last().unwrap().span, Number::from_f64(num_string.parse::().unwrap()), )) } else if let Some(hex_int) = self.match_exact(TokenKind::IntegerWithBase(16)) { let span = self.last().unwrap().span; Ok(Expression::Scalar( span, Number::from_f64( i128::from_str_radix(&hex_int.lexeme[2..].replace('_', ""), 16) .or_else(|_| overflow_error(span))? as f64, // TODO: i128 limits our precision here ), )) } else if let Some(oct_int) = self.match_exact(TokenKind::IntegerWithBase(8)) { let span = self.last().unwrap().span; Ok(Expression::Scalar( span, Number::from_f64( i128::from_str_radix(&oct_int.lexeme[2..].replace('_', ""), 8) .or_else(|_| overflow_error(span))? as f64, // TODO: i128 limits our precision here ), )) } else if let Some(bin_int) = self.match_exact(TokenKind::IntegerWithBase(2)) { let span = self.last().unwrap().span; Ok(Expression::Scalar( span, Number::from_f64( i128::from_str_radix(&bin_int.lexeme[2..].replace('_', ""), 2) .or_else(|_| overflow_error(span))? as f64, // TODO: i128 limits our precision here ), )) } else if let Some(identifier) = self.match_exact(TokenKind::Identifier) { let span = self.last().unwrap().span; Ok(Expression::Identifier(span, identifier.lexeme.clone())) } else if let Some(inner) = self.match_any(&[TokenKind::True, TokenKind::False]) { Ok(Expression::Boolean( inner.span, matches!(inner.kind, TokenKind::True), )) } else if let Some(token) = self.match_exact(TokenKind::StringFixed) { Ok(Expression::String( token.span, vec![StringPart::Fixed(strip_first_and_last(&token.lexeme))], )) } else if let Some(token) = self.match_exact(TokenKind::StringInterpolationStart) { let mut parts = vec![StringPart::Fixed(strip_first_and_last(&token.lexeme))]; let expr = self.expression()?; parts.push(StringPart::Interpolation(expr.full_span(), Box::new(expr))); let mut span_full_string = token.span; let mut has_end = false; while let Some(inner_token) = self.match_any(&[ TokenKind::StringInterpolationMiddle, TokenKind::StringInterpolationEnd, ]) { span_full_string = span_full_string.extend(&inner_token.span); match inner_token.kind { TokenKind::StringInterpolationMiddle => { parts.push(StringPart::Fixed(strip_first_and_last(&inner_token.lexeme))); let expr = self.expression()?; parts.push(StringPart::Interpolation(expr.full_span(), Box::new(expr))); } TokenKind::StringInterpolationEnd => { parts.push(StringPart::Fixed(strip_first_and_last(&inner_token.lexeme))); has_end = true; break; } _ => unreachable!(), } } if !has_end { span_full_string = span_full_string.extend(&self.last().unwrap().span); return Err(ParseError::new( ParseErrorKind::UnterminatedString, span_full_string, )); } parts.retain(|p| !matches!(p, StringPart::Fixed(s) if s.is_empty())); Ok(Expression::String(span_full_string, parts)) } else if self.match_exact(TokenKind::LeftParen).is_some() { let inner = self.expression()?; if self.match_exact(TokenKind::RightParen).is_none() { return Err(ParseError::new( ParseErrorKind::MissingClosingParen, self.peek().span, )); } Ok(inner) } else if matches!( self.peek().kind, TokenKind::ProcedurePrint | TokenKind::ProcedureAssertEq ) { Err(ParseError::new( ParseErrorKind::InlineProcedureUsage, self.peek().span, )) } else { Err(ParseError::new( ParseErrorKind::ExpectedPrimary, self.peek().span, )) } } /// Returns true iff the upcoming token indicates the beginning of a 'power' /// expression (which needs to start with a 'primary' expression). fn next_token_could_start_power_expression(&self) -> bool { // This function needs to be kept in sync with `primary` above. matches!( self.peek().kind, TokenKind::Number | TokenKind::Identifier | TokenKind::LeftParen ) } fn type_annotation(&mut self) -> Result { if let Some(token) = self.match_exact(TokenKind::Bool) { Ok(TypeAnnotation::Bool(token.span)) } else if let Some(token) = self.match_exact(TokenKind::String) { Ok(TypeAnnotation::String(token.span)) } else if let Some(token) = self.match_exact(TokenKind::DateTime) { Ok(TypeAnnotation::DateTime(token.span)) } else if self.match_exact(TokenKind::CapitalFn).is_some() { let span = self.last().unwrap().span; if self.match_exact(TokenKind::LeftBracket).is_none() { return Err(ParseError::new( ParseErrorKind::ExpectedTokenInFunctionType("left bracket"), self.peek().span, )); } if self.match_exact(TokenKind::LeftParen).is_none() { return Err(ParseError::new( ParseErrorKind::ExpectedTokenInFunctionType("left parenthesis"), self.peek().span, )); } let mut params = vec![]; if self.peek().kind != TokenKind::RightParen { params.push(self.type_annotation()?); while self.match_exact(TokenKind::Comma).is_some() { params.push(self.type_annotation()?); } } if self.match_exact(TokenKind::RightParen).is_none() { return Err(ParseError::new( ParseErrorKind::MissingClosingParen, self.peek().span, )); } if self.match_exact(TokenKind::Arrow).is_none() { return Err(ParseError::new( ParseErrorKind::ExpectedTokenInFunctionType("arrow (->)"), self.peek().span, )); } let return_type = self.type_annotation()?; if self.match_exact(TokenKind::RightBracket).is_none() { return Err(ParseError::new( ParseErrorKind::ExpectedTokenInFunctionType("right bracket"), self.peek().span, )); } let span = span.extend(&self.last().unwrap().span); Ok(TypeAnnotation::Fn(span, params, Box::new(return_type))) } else { Ok(TypeAnnotation::DimensionExpression( self.dimension_expression()?, )) } } fn dimension_expression(&mut self) -> Result { self.dimension_factor() } fn dimension_factor(&mut self) -> Result { let mut expr = self.dimension_power()?; while let Some(operator_token) = self.match_any(&[TokenKind::Multiply, TokenKind::Divide]) { let span = self.last().unwrap().span; let rhs = self.dimension_power()?; expr = if operator_token.kind == TokenKind::Multiply { DimensionExpression::Multiply(span, Box::new(expr), Box::new(rhs)) } else { DimensionExpression::Divide(span, Box::new(expr), Box::new(rhs)) }; } Ok(expr) } fn dimension_power(&mut self) -> Result { let expr = self.dimension_primary()?; if self.match_exact(TokenKind::Power).is_some() { let span = self.last().unwrap().span; let (span_exponent, exponent) = self.dimension_exponent()?; Ok(DimensionExpression::Power( Some(span), Box::new(expr), span_exponent, exponent, )) } else if let Some(exponent) = self.match_exact(TokenKind::UnicodeExponent) { let span_exponent = self.last().unwrap().span; let exp = Self::unicode_exponent_to_int(exponent.lexeme.as_str()); Ok(DimensionExpression::Power( None, Box::new(expr), span_exponent, Exponent::from_integer(exp as i128), )) } else { Ok(expr) } } fn dimension_exponent(&mut self) -> Result<(Span, Exponent)> { if let Some(token) = self.match_exact(TokenKind::Number) { let span = self.last().unwrap().span; let num_str = token.lexeme.replace('_', ""); Ok(( span, Rational::from_i128(num_str.parse::().map_err(|_| ParseError { kind: ParseErrorKind::NumberInDimensionExponentOutOfRange, span: token.span, })?) .ok_or(ParseError { kind: ParseErrorKind::NumberInDimensionExponentOutOfRange, span: token.span, })?, )) } else if self.match_exact(TokenKind::Minus).is_some() { let span = self.last().unwrap().span; let (span_inner, exponent) = self.dimension_exponent()?; Ok((span.extend(&span_inner), -exponent)) } else if self.match_exact(TokenKind::LeftParen).is_some() { let mut span = self.last().unwrap().span; let (span_inner, exponent) = self.dimension_exponent()?; span = span.extend(&span_inner); if self.match_exact(TokenKind::RightParen).is_some() { span = span.extend(&self.last().unwrap().span); Ok((span, exponent)) } else if self.match_exact(TokenKind::Divide).is_some() { let (span_rhs, rhs) = self.dimension_exponent()?; span = span.extend(&span_rhs); if rhs == Rational::zero() { Err(ParseError::new( ParseErrorKind::DivisionByZeroInDimensionExponent, self.last().unwrap().span, )) } else if self.match_exact(TokenKind::RightParen).is_none() { Err(ParseError::new( ParseErrorKind::MissingClosingParen, self.peek().span, )) } else { span = span.extend(&self.last().unwrap().span); Ok(( span, exponent.checked_div(&rhs).ok_or_else(|| { ParseError::new(ParseErrorKind::OverflowInDimensionExponent, span) })?, )) } } else { Err(ParseError::new( ParseErrorKind::MissingClosingParen, self.peek().span, )) } } else { Err(ParseError::new( ParseErrorKind::ExpectedDimensionExponent, self.peek().span, )) } } fn dimension_primary(&mut self) -> Result { let e = Err(ParseError::new( ParseErrorKind::ExpectedDimensionPrimary, self.peek().span, )); if let Some(token) = self.match_exact(TokenKind::Identifier) { if token.lexeme.starts_with("__") { return Err(ParseError::new( ParseErrorKind::DoubleUnderscoreTypeNamesReserved, token.span, )); } let span = self.last().unwrap().span; Ok(DimensionExpression::Dimension(span, token.lexeme.clone())) } else if let Some(number) = self.match_exact(TokenKind::Number) { let span = self.last().unwrap().span; if number.lexeme != "1" { e } else { Ok(DimensionExpression::Unity(span)) } } else if self.match_exact(TokenKind::LeftParen).is_some() { let dexpr = self.dimension_expression()?; if self.match_exact(TokenKind::RightParen).is_none() { return Err(ParseError::new( ParseErrorKind::MissingClosingParen, self.peek().span, )); } Ok(dexpr) } else { e } } fn match_exact(&mut self, token_kind: TokenKind) -> Option<&'a Token> { let token = self.peek(); if token.kind == token_kind { self.advance(); Some(token) } else { None } } fn match_any(&mut self, kinds: &[TokenKind]) -> Option<&'a Token> { for kind in kinds { if let result @ Some(..) = self.match_exact(*kind) { return result; } } None } fn advance(&mut self) { if !self.is_at_end() { self.current += 1; } } fn peek(&self) -> &'a Token { &self.tokens[self.current] } fn last(&self) -> Option<&'a Token> { self.tokens.get(self.current - 1) } pub fn is_end_of_statement(&self) -> bool { self.peek().kind == TokenKind::Newline || self.is_at_end() } pub fn is_at_end(&self) -> bool { self.peek().kind == TokenKind::Eof } } fn strip_first_and_last(s: &str) -> String { s[1..(s.len() - 1)].to_string() } /// Parse a string. /// If an error is encountered and `stop_on_error` is set to false, the parser /// will try to recover from the error and parse as many statements as possible /// while stacking all the errors in a `Vec`. At the end, it returns the complete /// list of statements parsed + the list of errors accumulated. pub fn parse(input: &str, code_source_id: usize) -> ParseResult { use crate::tokenizer::tokenize; let tokens = tokenize(input, code_source_id) .map_err(|TokenizerError { kind, span }| { ParseError::new(ParseErrorKind::TokenizerError(kind), span) }) .map_err(|e| (Vec::new(), vec![e]))?; let mut parser = Parser::new(&tokens); parser.parse() } #[cfg(test)] pub fn parse_dexpr(input: &str) -> DimensionExpression { let tokens = crate::tokenizer::tokenize(input, 0).expect("No tokenizer errors in tests"); let mut parser = crate::parser::Parser::new(&tokens); let expr = parser .dimension_expression() .expect("No parser errors in tests"); assert!(parser.is_at_end()); expr } #[cfg(test)] mod tests { use insta::assert_snapshot; use std::fmt::Write; use super::*; use crate::ast::{ binop, boolean, conditional, factorial, identifier, logical_neg, negate, scalar, ReplaceSpans, }; #[track_caller] fn parse_as(inputs: &[&str], statement_expected: Statement) { for input in inputs { let statements = parse(input, 0).expect("parse error").replace_spans(); assert!(statements.len() == 1); let statement = &statements[0]; assert_eq!(*statement, statement_expected); } } #[track_caller] fn parse_as_expression(inputs: &[&str], expr_expected: Expression) { parse_as(inputs, Statement::Expression(expr_expected)); } #[track_caller] fn should_fail(inputs: &[&str]) { for input in inputs { assert!(parse(input, 0).is_err()); } } #[track_caller] fn should_fail_with(inputs: &[&str], error_kind: ParseErrorKind) { for input in inputs { match parse(input, 0) { Err((_, errors)) => { assert_eq!(errors[0].kind, error_kind); } _ => { panic!(); } } } } #[track_caller] fn snap_parse(input: impl AsRef) -> String { let mut ret = String::new(); match parse(input.as_ref(), 0) { Ok(stmts) => { for stmt in stmts { writeln!(&mut ret, "{stmt:?}").unwrap(); } } Err((stmts, errors)) => { writeln!(&mut ret, "Successfully parsed:").unwrap(); for stmt in stmts { writeln!(&mut ret, "{stmt:?}").unwrap(); } writeln!(&mut ret, "Errors encountered:").unwrap(); for error in errors { writeln!(&mut ret, "{error} - {error:?}").unwrap(); } } } ret } #[test] fn invalid_input() { should_fail(&["+", "->", "§"]); should_fail_with( &["1)", "(1))"], ParseErrorKind::TrailingCharacters(")".into()), ); } #[test] fn numbers_simple() { parse_as_expression(&["1", "1.0", " 1 ", " 1.0000 ", "1."], scalar!(1.0)); parse_as_expression(&["0.2", " 0.2 ", ".2"], scalar!(0.2)); parse_as_expression(&["3.5", " 3.5 ", "3.50"], scalar!(3.5)); parse_as_expression(&["0.05"], scalar!(0.05)); parse_as_expression(&["123.456"], scalar!(123.456)); should_fail(&[ "123..", "0..", ".0.", ".", ". 2", "..2", "..", ".1.1", "0.1.", ]); } #[test] fn large_numbers() { parse_as_expression( &["1234567890000000", "1234567890000000.0"], scalar!(1234567890000000.0), ); } #[test] fn decimal_separator() { parse_as_expression( &["50_000_000", "50_000_000.0", "50_000000"], scalar!(50_000_000.0), ); parse_as_expression(&["1_000"], scalar!(1000.0)); parse_as_expression(&["1.000001", "1.000_001"], scalar!(1.000_001)); parse_as_expression(&["1e1_0_0"], scalar!(1e100)); // Leading underscores are not allowed / will result in parsing as identifier parse_as_expression(&["_50_000_000"], identifier!("_50_000_000")); should_fail(&["1._0", "1e+_0", "1e-_0"]); // Trailing underscores are not allowed should_fail(&["100_", "1.00_", "1e2_"]); } #[test] fn factorials() { parse_as_expression( &["4!", "4.0!", "4 !", " 4 !", "(4)!"], factorial!(scalar!(4.0)), ); parse_as_expression( &["3!^3", "(3!)^3"], binop!(factorial!(scalar!(3.0)), Power, scalar!(3.0)), ); parse_as_expression( &["3²!"], factorial!(binop!(scalar!(3.0), Power, scalar!(2.0))), ); parse_as_expression( &["3^3!"], binop!(scalar!(3.0), Power, factorial!(scalar!(3.0))), ); parse_as_expression( &["-5!", "-(5!)", "-(5)!"], negate!(factorial!(scalar!(5.0))), ); parse_as_expression(&["5!!", "(5!)!"], factorial!(factorial!(scalar!(5.0)))); } #[test] fn unary() { parse_as_expression(&["-1", " - 1 "], negate!(scalar!(1.0))); parse_as_expression(&["-123.45"], negate!(scalar!(123.45))); parse_as_expression(&["--1", " - - 1 "], negate!(negate!(scalar!(1.0)))); parse_as_expression(&["-x", " - x"], negate!(identifier!("x"))); parse_as_expression(&["-0.61", "-.61", "- .61"], negate!(scalar!(0.61))); parse_as_expression( &["-1 + 2"], binop!(negate!(scalar!(1.0)), Add, scalar!(2.0)), ); parse_as_expression(&["+1", " + 1 "], scalar!(1.0)); parse_as_expression(&["+123.45"], scalar!(123.45)); parse_as_expression(&["++1", " + + 1 "], scalar!(1.0)); parse_as_expression(&["+x", " + x"], identifier!("x")); parse_as_expression(&["+0.61", "+.61", "+ .61"], scalar!(0.61)); parse_as_expression( &["+1 + 2", "1 +++++ 2"], binop!(scalar!(1.0), Add, scalar!(2.0)), ); parse_as_expression( &["+1 - 2", "+1 - +2 "], binop!(scalar!(1.0), Sub, scalar!(2.0)), ); } #[test] fn scientific_notation() { parse_as_expression(&["1e3"], scalar!(1.0e3)); parse_as_expression(&["1e+3"], scalar!(1.0e+3)); parse_as_expression(&["1e-3"], scalar!(1.0e-3)); parse_as_expression(&["-1e-3"], negate!(scalar!(1.0e-3))); parse_as_expression(&["123.456e12"], scalar!(123.456e12)); parse_as_expression(&["123.456e+12"], scalar!(123.456e+12)); parse_as_expression(&["123.456e-12"], scalar!(123.456e-12)); should_fail(&["1e++2", "1e+-2", "1e+", "1e-"]); should_fail(&["2e1.5", "e.2e3e"]); parse_as_expression(&["1e", "1.0e"], binop!(scalar!(1.0), Mul, identifier!("e"))); parse_as_expression(&["1ee"], binop!(scalar!(1.0), Mul, identifier!("ee"))); parse_as_expression(&["1eV"], binop!(scalar!(1.0), Mul, identifier!("eV"))); parse_as_expression(&["1erg"], binop!(scalar!(1.0), Mul, identifier!("erg"))); } #[test] fn hex_oct_bin() { parse_as_expression(&["0x6A", "0x6a", "0b1101010", "0o152"], scalar!(106.0)); parse_as_expression(&["0xFF", "0xff", "0b11111111", "0o377"], scalar!(255.0)); parse_as_expression(&["-0x3", "-0b11", "-0o3"], negate!(scalar!(3.0))); should_fail(&["0x"]); should_fail(&["0o"]); should_fail(&["0b"]); should_fail(&["0xG"]); should_fail(&["0oG"]); should_fail(&["0oA"]); should_fail(&["0o8"]); should_fail(&["0bF"]); should_fail(&["0b2"]); should_fail(&["0xABCDU"]); should_fail(&["0o12348"]); should_fail(&["0b10102"]); // Until we support hexadecimal float notation should_fail(&["0x1.2", "0b1.0", "0o1.0", "0x.1", "0b.0", "0o.1"]); } #[test] fn identifiers() { parse_as_expression(&["foo", " foo "], identifier!("foo")); parse_as_expression(&["foo_bar"], identifier!("foo_bar")); parse_as_expression(&["MeineSchöneVariable"], identifier!("MeineSchöneVariable")); parse_as_expression(&["°"], identifier!("°")); parse_as_expression(&["Mass_H₂O"], identifier!("Mass_H₂O")); } #[test] fn addition_and_subtraction() { parse_as_expression( &["1+2", " 1 + 2 "], binop!(scalar!(1.0), Add, scalar!(2.0)), ); // Minus should be left-associative parse_as_expression( &["1-2-3", "1 - 2 - 3", "(1-2)-3"], binop!(binop!(scalar!(1.0), Sub, scalar!(2.0)), Sub, scalar!(3.0)), ); should_fail(&["1+", "1-"]); } #[test] fn multiplication_and_division() { parse_as_expression( &["1*2", " 1 * 2 ", "1 · 2", "1 × 2"], binop!(scalar!(1.0), Mul, scalar!(2.0)), ); parse_as_expression( &["1/2", "1 per 2", "1÷2"], binop!(scalar!(1.0), Div, scalar!(2.0)), ); parse_as_expression( &["1/2/3", "(1/2)/3", "1 per 2 per 3"], binop!(binop!(scalar!(1.0), Div, scalar!(2.0)), Div, scalar!(3.0)), ); should_fail(&["1*@", "1*", "1 per", "÷", "×"]); // 'per' is higher-precedence than '/' parse_as_expression( &["1 / meter per second"], binop!( scalar!(1.0), Div, binop!(identifier!("meter"), Div, identifier!("second")) ), ); } #[test] fn addition_subtraction_multiplication_division_precedence() { parse_as_expression( &["5+3*2", "5+(3*2)"], binop!(scalar!(5.0), Add, binop!(scalar!(3.0), Mul, scalar!(2.0))), ); parse_as_expression( &["5/3*2", "(5/3)*2"], binop!(binop!(scalar!(5.0), Div, scalar!(3.0)), Mul, scalar!(2.0)), ); should_fail(&["3+*4", "3*/4", "(3+4", "3+4)", "3+(", "()", "(3+)4"]) } #[test] fn implicit_multiplication() { parse_as_expression( &["1 2", " 1 2 "], binop!(scalar!(1.0), Mul, scalar!(2.0)), ); parse_as_expression( &["2 meter"], binop!(scalar!(2.0), Mul, identifier!("meter")), ); } #[test] fn exponentiation() { parse_as_expression( &["2^3", " 2 ^ 3 ", "2**3"], binop!(scalar!(2.0), Power, scalar!(3.0)), ); parse_as_expression( &["2^3^4", " 2 ^ 3 ^ 4 ", "2**3**4"], binop!( scalar!(2.0), Power, binop!(scalar!(3.0), Power, scalar!(4.0)) ), ); parse_as_expression( &["(2^3)^4", "(2**3)**4"], binop!( binop!(scalar!(2.0), Power, scalar!(3.0)), Power, scalar!(4.0) ), ); parse_as_expression( &["2^-3", "2**-3"], binop!(scalar!(2.0), Power, negate!(scalar!(3.0))), ); parse_as_expression( &["2^-3^-4"], binop!( scalar!(2.0), Power, negate!(binop!(scalar!(3.0), Power, negate!(scalar!(4.0)))) ), ); parse_as_expression( &["2^-2-3"], binop!( binop!(scalar!(2.0), Power, negate!(scalar!(2.0))), Sub, scalar!(3.0) ), ); should_fail(&[ "1^", "1^^2", "1**", "1***3", "1****4", "2^-", "2^--", "2**--3", ]); } #[test] fn unicode_exponentiation() { parse_as_expression(&["2³"], binop!(scalar!(2.0), Power, scalar!(3.0))); parse_as_expression(&["2⁻⁴"], binop!(scalar!(2.0), Power, scalar!(-4.0))); parse_as_expression( &["(2^3)²"], binop!( binop!(scalar!(2.0), Power, scalar!(3.0)), Power, scalar!(2.0) ), ); parse_as_expression( &["2⁵^4"], binop!( binop!(scalar!(2.0), Power, scalar!(5.0)), Power, scalar!(4.0) ), ); should_fail(&["1²³", "2⁻", "2⁻3", "²", "²3"]); } #[test] fn conversion() { parse_as_expression( &["1->2", "1→2"], binop!(scalar!(1.0), ConvertTo, scalar!(2.0)), ); // Conversion is left-associative parse_as_expression( &["1→2→3"], binop!( binop!(scalar!(1.0), ConvertTo, scalar!(2.0)), ConvertTo, scalar!(3.0) ), ); should_fail(&["1 - > 2", "1 -> -> 2"]); } #[test] fn variable_definition() { parse_as( &["let foo = 1", "let foo=1"], Statement::DefineVariable { identifier_span: Span::dummy(), identifier: "foo".into(), expr: scalar!(1.0), type_annotation: None, decorators: Vec::new(), }, ); parse_as( &["let x: Length = 1 * meter"], Statement::DefineVariable { identifier_span: Span::dummy(), identifier: "x".into(), expr: binop!(scalar!(1.0), Mul, identifier!("meter")), type_annotation: Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "Length".into()), )), decorators: Vec::new(), }, ); // same as above, but with some decorators parse_as( &["@name(\"myvar\") @aliases(foo, bar) let x: Length = 1 * meter"], Statement::DefineVariable { identifier_span: Span::dummy(), identifier: "x".into(), expr: binop!(scalar!(1.0), Mul, identifier!("meter")), type_annotation: Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "Length".into()), )), decorators: vec![ decorator::Decorator::Name("myvar".into()), decorator::Decorator::Aliases(vec![("foo".into(), None), ("bar".into(), None)]), ], }, ); should_fail_with( &["let (foo)=2", "let 2=3", "let = 2"], ParseErrorKind::ExpectedIdentifierAfterLet, ); should_fail_with( &["let foo", "let foo 2"], ParseErrorKind::ExpectedEqualOrColonAfterLetIdentifier, ); should_fail_with( &["foo = 2"], ParseErrorKind::TrailingEqualSign("foo".into()), ); should_fail(&["let x²=2", "let x+y=2", "let 3=5", "let x=", "let x"]); should_fail_with( &["@aliases(foo, f: short) let foobar = 1"], ParseErrorKind::DecoratorsWithPrefixOnLetDefinition, ); } #[test] fn dimension_definition() { parse_as( &["dimension px"], Statement::DefineDimension("px".into(), vec![]), ); parse_as( &[ "dimension Area = Length * Length", "dimension Area = Length × Length", "dimension Area =\n Length × Length", ], Statement::DefineDimension( "Area".into(), vec![DimensionExpression::Multiply( Span::dummy(), Box::new(DimensionExpression::Dimension( Span::dummy(), "Length".into(), )), Box::new(DimensionExpression::Dimension( Span::dummy(), "Length".into(), )), )], ), ); parse_as( &["dimension Velocity = Length / Time"], Statement::DefineDimension( "Velocity".into(), vec![DimensionExpression::Divide( Span::dummy(), Box::new(DimensionExpression::Dimension( Span::dummy(), "Length".into(), )), Box::new(DimensionExpression::Dimension(Span::dummy(), "Time".into())), )], ), ); parse_as( &["dimension Area = Length^2"], Statement::DefineDimension( "Area".into(), vec![DimensionExpression::Power( Some(Span::dummy()), Box::new(DimensionExpression::Dimension( Span::dummy(), "Length".into(), )), Span::dummy(), Rational::from_integer(2), )], ), ); parse_as( &["dimension Energy = Mass * Length^2 / Time^2"], Statement::DefineDimension( "Energy".into(), vec![DimensionExpression::Divide( Span::dummy(), Box::new(DimensionExpression::Multiply( Span::dummy(), Box::new(DimensionExpression::Dimension(Span::dummy(), "Mass".into())), Box::new(DimensionExpression::Power( Some(Span::dummy()), Box::new(DimensionExpression::Dimension( Span::dummy(), "Length".into(), )), Span::dummy(), Rational::from_integer(2), )), )), Box::new(DimensionExpression::Power( Some(Span::dummy()), Box::new(DimensionExpression::Dimension(Span::dummy(), "Time".into())), Span::dummy(), Rational::from_integer(2), )), )], ), ); parse_as( &["dimension X = Length^(12345/67890)"], Statement::DefineDimension( "X".into(), vec![DimensionExpression::Power( Some(Span::dummy()), Box::new(DimensionExpression::Dimension( Span::dummy(), "Length".into(), )), Span::dummy(), Rational::new(12345, 67890), )], ), ); // Regression test, found using fuzzing. This should result in an error, but not panic should_fail_with( &["dimension X = Length^(6/(5/(99999999999999999999999999999999999999)))"], ParseErrorKind::OverflowInDimensionExponent, ); } #[test] fn function_definition() { parse_as( &["fn foo() = 1", "fn foo() =\n 1"], Statement::DefineFunction { function_name_span: Span::dummy(), function_name: "foo".into(), type_parameters: vec![], parameters: vec![], body: Some(scalar!(1.0)), return_type_annotation_span: None, return_type_annotation: None, }, ); parse_as( &["fn foo() -> Scalar = 1"], Statement::DefineFunction { function_name_span: Span::dummy(), function_name: "foo".into(), type_parameters: vec![], parameters: vec![], body: Some(scalar!(1.0)), return_type_annotation_span: Some(Span::dummy()), return_type_annotation: Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "Scalar".into()), )), }, ); parse_as( &["fn foo(x) = 1"], Statement::DefineFunction { function_name_span: Span::dummy(), function_name: "foo".into(), type_parameters: vec![], parameters: vec![(Span::dummy(), "x".into(), None, false)], body: Some(scalar!(1.0)), return_type_annotation_span: None, return_type_annotation: None, }, ); parse_as( &["fn foo(x, y, z) = 1"], Statement::DefineFunction { function_name_span: Span::dummy(), function_name: "foo".into(), type_parameters: vec![], parameters: vec![ (Span::dummy(), "x".into(), None, false), (Span::dummy(), "y".into(), None, false), (Span::dummy(), "z".into(), None, false), ], body: Some(scalar!(1.0)), return_type_annotation_span: None, return_type_annotation: None, }, ); parse_as( &["fn foo(x: Length, y: Time, z: Length^3 · Time^2) -> Scalar = 1"], Statement::DefineFunction { function_name_span: Span::dummy(), function_name: "foo".into(), type_parameters: vec![], parameters: vec![ ( Span::dummy(), "x".into(), Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "Length".into()), )), false, ), ( Span::dummy(), "y".into(), Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "Time".into()), )), false, ), ( Span::dummy(), "z".into(), Some(TypeAnnotation::DimensionExpression( DimensionExpression::Multiply( Span::dummy(), Box::new(DimensionExpression::Power( Some(Span::dummy()), Box::new(DimensionExpression::Dimension( Span::dummy(), "Length".into(), )), Span::dummy(), Rational::new(3, 1), )), Box::new(DimensionExpression::Power( Some(Span::dummy()), Box::new(DimensionExpression::Dimension( Span::dummy(), "Time".into(), )), Span::dummy(), Rational::new(2, 1), )), ), )), false, ), ], body: Some(scalar!(1.0)), return_type_annotation_span: Some(Span::dummy()), return_type_annotation: Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "Scalar".into()), )), }, ); parse_as( &["fn foo(x: X) = 1"], Statement::DefineFunction { function_name_span: Span::dummy(), function_name: "foo".into(), type_parameters: vec![(Span::dummy(), "X".into())], parameters: vec![( Span::dummy(), "x".into(), Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "X".into()), )), false, )], body: Some(scalar!(1.0)), return_type_annotation_span: None, return_type_annotation: None, }, ); parse_as( &["fn foo(x: D…) -> D"], Statement::DefineFunction { function_name_span: Span::dummy(), function_name: "foo".into(), type_parameters: vec![(Span::dummy(), "D".into())], parameters: vec![( Span::dummy(), "x".into(), Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "D".into()), )), true, )], body: None, return_type_annotation_span: Some(Span::dummy()), return_type_annotation: Some(TypeAnnotation::DimensionExpression( DimensionExpression::Dimension(Span::dummy(), "D".into()), )), }, ); should_fail_with( &[ "fn foo(x: D, y: D…) -> D", "fn foo(y: D…, x: D) -> D", "fn foo(y: D…, x: D…) -> D", ], ParseErrorKind::OnlySingleVariadicParameter, ); should_fail_with( &[ "fn foo(x: Scalar…) -> Scalar = 1", "fn foo(x: D…) -> 1 = 1", ], ParseErrorKind::VariadicParameterOnlyAllowedInForeignFunction, ); } #[test] fn function_call() { parse_as_expression( &["foo()"], Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(identifier!("foo")), vec![], ), ); parse_as_expression( &["foo(1)"], Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(identifier!("foo")), vec![scalar!(1.0)], ), ); parse_as_expression( &["foo(1,2,3)"], Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(identifier!("foo")), vec![scalar!(1.0), scalar!(2.0), scalar!(3.0)], ), ); should_fail(&["exp(,)", "exp(1,)"]) } #[test] fn callable_calls() { parse_as_expression( &["(returns_fn())()"], Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(identifier!("returns_fn")), vec![], )), vec![], ), ); parse_as_expression( &["returns_fn()()"], Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(identifier!("returns_fn")), vec![], )), vec![], ), ); } #[test] fn postfix_apply() { parse_as_expression( &["1 + 1 // foo"], Expression::FunctionCall( Span::dummy(), Span::dummy(), Box::new(identifier!("foo")), vec![binop!(scalar!(1.0), Add, scalar!(1.0))], ), ); } #[test] fn procedure_call() { parse_as( &["print(2)"], Statement::ProcedureCall(Span::dummy(), ProcedureKind::Print, vec![scalar!(2.0)]), ); parse_as( &["print(2, 3, 4)"], Statement::ProcedureCall( Span::dummy(), ProcedureKind::Print, vec![scalar!(2.0), scalar!(3.0), scalar!(4.0)], ), ); should_fail_with( &["print", "print 2"], ParseErrorKind::ExpectedLeftParenAfterProcedureName, ); should_fail_with(&["1+print(2)"], ParseErrorKind::InlineProcedureUsage); should_fail_with( &["fn print() = 1"], ParseErrorKind::ExpectedIdentifierAfterFn, ); } #[test] fn logical_operation() { // basic assert_snapshot!(snap_parse( "true || false"), @r###" Expression(BinaryOperator { op: LogicalOr, lhs: Boolean(Span { start: SourceCodePositition { byte: 0, line: 1, position: 1 }, end: SourceCodePositition { byte: 4, line: 1, position: 5 }, code_source_id: 0 }, true), rhs: Boolean(Span { start: SourceCodePositition { byte: 8, line: 1, position: 9 }, end: SourceCodePositition { byte: 13, line: 1, position: 14 }, code_source_id: 0 }, false), span_op: Some(Span { start: SourceCodePositition { byte: 5, line: 1, position: 6 }, end: SourceCodePositition { byte: 7, line: 1, position: 8 }, code_source_id: 0 }) }) "###); assert_snapshot!(snap_parse( "true && false"), @r###" Expression(BinaryOperator { op: LogicalAnd, lhs: Boolean(Span { start: SourceCodePositition { byte: 0, line: 1, position: 1 }, end: SourceCodePositition { byte: 4, line: 1, position: 5 }, code_source_id: 0 }, true), rhs: Boolean(Span { start: SourceCodePositition { byte: 8, line: 1, position: 9 }, end: SourceCodePositition { byte: 13, line: 1, position: 14 }, code_source_id: 0 }, false), span_op: Some(Span { start: SourceCodePositition { byte: 5, line: 1, position: 6 }, end: SourceCodePositition { byte: 7, line: 1, position: 8 }, code_source_id: 0 }) }) "###); assert_snapshot!(snap_parse( "!true"), @r###" Expression(UnaryOperator { op: LogicalNeg, expr: Boolean(Span { start: SourceCodePositition { byte: 1, line: 1, position: 2 }, end: SourceCodePositition { byte: 5, line: 1, position: 6 }, code_source_id: 0 }, true), span_op: Span { start: SourceCodePositition { byte: 0, line: 1, position: 1 }, end: SourceCodePositition { byte: 1, line: 1, position: 2 }, code_source_id: 0 } }) "###); // priority #[rustfmt::skip] parse_as_expression( &["true || false && true || false"], binop!( // the 'and' operator has the highest priority binop!(boolean!(true), LogicalOr, binop!(boolean!(false), LogicalAnd, boolean!(true))), LogicalOr, boolean!(false) ) ); #[rustfmt::skip] parse_as_expression( &["!true && false"], binop!( // The negation has precedence over the 'and' logical_neg!(boolean!(true)), LogicalAnd, boolean!(false) ) ); } #[test] fn comparisons() { parse_as_expression(&["1 < 2"], binop!(scalar!(1.0), LessThan, scalar!(2.0))); parse_as_expression( &["1 <= 2", "1 ≤ 2"], binop!(scalar!(1.0), LessOrEqual, scalar!(2.0)), ); parse_as_expression(&["1 > 2"], binop!(scalar!(1.0), GreaterThan, scalar!(2.0))); parse_as_expression( &["1 >= 2", "1 ≥ 2"], binop!(scalar!(1.0), GreaterOrEqual, scalar!(2.0)), ); parse_as_expression( &["1 == 2", "1 ⩵ 2"], binop!(scalar!(1.0), Equal, scalar!(2.0)), ); parse_as_expression( &["1 != 2", "1 ≠ 2"], binop!(scalar!(1.0), NotEqual, scalar!(2.0)), ); } #[test] fn conditionals() { parse_as_expression( &[ "if 1 < 2 then 3 + 4 else 5 * 6", "if (1 < 2) then (3 + 4) else (5 * 6)", "if 1 < 2\n then 3 + 4\n else 5 * 6", ], conditional!( binop!(scalar!(1.0), LessThan, scalar!(2.0)), binop!(scalar!(3.0), Add, scalar!(4.0)), binop!(scalar!(5.0), Mul, scalar!(6.0)) ), ); parse_as_expression( &[ "if true then if false then 1 else 2 else 3", "if true then (if false then 1 else 2) else 3", ], conditional!( Expression::Boolean(Span::dummy(), true), conditional!( Expression::Boolean(Span::dummy(), false), scalar!(1.0), scalar!(2.0) ), scalar!(3.0) ), ); should_fail_with(&["if true 1 else 2"], ParseErrorKind::ExpectedThen); should_fail_with(&["if true then 1"], ParseErrorKind::ExpectedElse); } #[test] fn strings() { parse_as_expression( &["\"hello world\""], Expression::String(Span::dummy(), vec![StringPart::Fixed("hello world".into())]), ); parse_as_expression( &["\"pi = {pi}\""], Expression::String( Span::dummy(), vec![ StringPart::Fixed("pi = ".into()), StringPart::Interpolation(Span::dummy(), Box::new(identifier!("pi"))), ], ), ); parse_as_expression( &["\"{pi}\""], Expression::String( Span::dummy(), vec![StringPart::Interpolation( Span::dummy(), Box::new(identifier!("pi")), )], ), ); parse_as_expression( &["\"{pi}{e}\""], Expression::String( Span::dummy(), vec![ StringPart::Interpolation(Span::dummy(), Box::new(identifier!("pi"))), StringPart::Interpolation(Span::dummy(), Box::new(identifier!("e"))), ], ), ); parse_as_expression( &["\"{pi} + {e}\""], Expression::String( Span::dummy(), vec![ StringPart::Interpolation(Span::dummy(), Box::new(identifier!("pi"))), StringPart::Fixed(" + ".into()), StringPart::Interpolation(Span::dummy(), Box::new(identifier!("e"))), ], ), ); parse_as_expression( &["\"1 + 2 = {1 + 2}\""], Expression::String( Span::dummy(), vec![ StringPart::Fixed("1 + 2 = ".into()), StringPart::Interpolation( Span::dummy(), Box::new(binop!(scalar!(1.0), Add, scalar!(2.0))), ), ], ), ); should_fail_with(&["\"test {1"], ParseErrorKind::UnterminatedString); } #[test] fn accumulate_errors() { // error on the last character of a line assert_snapshot!(snap_parse( "1 + 2 + 3"), @r###" Successfully parsed: Expression(BinaryOperator { op: Add, lhs: Scalar(Span { start: SourceCodePositition { byte: 17, line: 2, position: 13 }, end: SourceCodePositition { byte: 18, line: 2, position: 14 }, code_source_id: 0 }, Number(2.0)), rhs: Scalar(Span { start: SourceCodePositition { byte: 21, line: 2, position: 17 }, end: SourceCodePositition { byte: 22, line: 2, position: 18 }, code_source_id: 0 }, Number(3.0)), span_op: Some(Span { start: SourceCodePositition { byte: 19, line: 2, position: 15 }, end: SourceCodePositition { byte: 20, line: 2, position: 16 }, code_source_id: 0 }) }) Errors encountered: Expected one of: number, identifier, parenthesized expression - ParseError { kind: ExpectedPrimary, span: Span { start: SourceCodePositition { byte: 4, line: 1, position: 5 }, end: SourceCodePositition { byte: 5, line: 1, position: 6 }, code_source_id: 0 } } "###); // error in the middle of something assert_snapshot!(snap_parse( " let cool = 50 let tamo = * 30 assert_eq(tamo + cool == 80) 30m"), @r###" Successfully parsed: DefineVariable { identifier_span: Span { start: SourceCodePositition { byte: 17, line: 2, position: 17 }, end: SourceCodePositition { byte: 21, line: 2, position: 21 }, code_source_id: 0 }, identifier: "cool", expr: Scalar(Span { start: SourceCodePositition { byte: 24, line: 2, position: 24 }, end: SourceCodePositition { byte: 26, line: 2, position: 26 }, code_source_id: 0 }, Number(50.0)), type_annotation: None, decorators: [] } ProcedureCall(Span { start: SourceCodePositition { byte: 68, line: 4, position: 13 }, end: SourceCodePositition { byte: 77, line: 4, position: 22 }, code_source_id: 0 }, AssertEq, [BinaryOperator { op: Equal, lhs: BinaryOperator { op: Add, lhs: Identifier(Span { start: SourceCodePositition { byte: 78, line: 4, position: 23 }, end: SourceCodePositition { byte: 82, line: 4, position: 27 }, code_source_id: 0 }, "tamo"), rhs: Identifier(Span { start: SourceCodePositition { byte: 85, line: 4, position: 30 }, end: SourceCodePositition { byte: 89, line: 4, position: 34 }, code_source_id: 0 }, "cool"), span_op: Some(Span { start: SourceCodePositition { byte: 83, line: 4, position: 28 }, end: SourceCodePositition { byte: 84, line: 4, position: 29 }, code_source_id: 0 }) }, rhs: Scalar(Span { start: SourceCodePositition { byte: 93, line: 4, position: 38 }, end: SourceCodePositition { byte: 95, line: 4, position: 40 }, code_source_id: 0 }, Number(80.0)), span_op: Some(Span { start: SourceCodePositition { byte: 90, line: 4, position: 35 }, end: SourceCodePositition { byte: 92, line: 4, position: 37 }, code_source_id: 0 }) }]) Expression(BinaryOperator { op: Mul, lhs: Scalar(Span { start: SourceCodePositition { byte: 109, line: 5, position: 13 }, end: SourceCodePositition { byte: 111, line: 5, position: 15 }, code_source_id: 0 }, Number(30.0)), rhs: Identifier(Span { start: SourceCodePositition { byte: 111, line: 5, position: 15 }, end: SourceCodePositition { byte: 112, line: 5, position: 16 }, code_source_id: 0 }, "m"), span_op: None }) Errors encountered: Expected one of: number, identifier, parenthesized expression - ParseError { kind: ExpectedPrimary, span: Span { start: SourceCodePositition { byte: 50, line: 3, position: 24 }, end: SourceCodePositition { byte: 51, line: 3, position: 25 }, code_source_id: 0 } } "###); // error on a multiline let assert_snapshot!(snap_parse( " let tamo = * cool "), @r###" Successfully parsed: Errors encountered: Expected one of: number, identifier, parenthesized expression - ParseError { kind: ExpectedPrimary, span: Span { start: SourceCodePositition { byte: 40, line: 3, position: 17 }, end: SourceCodePositition { byte: 41, line: 3, position: 18 }, code_source_id: 0 } } "###); // error on a multiline if assert_snapshot!(snap_parse( " if a = then false else true "), @r###" Successfully parsed: Errors encountered: Expected 'then' in if-then-else condition - ParseError { kind: ExpectedThen, span: Span { start: SourceCodePositition { byte: 18, line: 2, position: 18 }, end: SourceCodePositition { byte: 19, line: 2, position: 19 }, code_source_id: 0 } } Expected one of: number, identifier, parenthesized expression - ParseError { kind: ExpectedPrimary, span: Span { start: SourceCodePositition { byte: 36, line: 3, position: 17 }, end: SourceCodePositition { byte: 40, line: 3, position: 21 }, code_source_id: 0 } } Expected one of: number, identifier, parenthesized expression - ParseError { kind: ExpectedPrimary, span: Span { start: SourceCodePositition { byte: 63, line: 4, position: 17 }, end: SourceCodePositition { byte: 67, line: 4, position: 21 }, code_source_id: 0 } } "###); // #260 assert_snapshot!(snap_parse( "x = 3"), @r###" Successfully parsed: Expression(Identifier(Span { start: SourceCodePositition { byte: 0, line: 1, position: 1 }, end: SourceCodePositition { byte: 1, line: 1, position: 2 }, code_source_id: 0 }, "x")) Errors encountered: Trailing '=' sign. Use `let x = …` if you intended to define a new constant. - ParseError { kind: TrailingEqualSign("x"), span: Span { start: SourceCodePositition { byte: 2, line: 1, position: 3 }, end: SourceCodePositition { byte: 3, line: 1, position: 4 }, code_source_id: 0 } } "###); } } numbat-1.11.0/src/prefix.rs000064400000000000000000000130311046102023000136220ustar 00000000000000use crate::number::Number; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Prefix { /// Represents a metric/decimal prefix symbolizing 10^n Metric(i32), /// Represents a binary prefix symbolizing 2^n Binary(i32), } impl Prefix { pub fn factor(&self) -> Number { match self { Prefix::Metric(exp) => Number::from_f64(10.0f64.powi(*exp)), Prefix::Binary(exp) => Number::from_f64(2.0f64.powi(*exp)), } } pub fn none() -> Self { Prefix::Metric(0) } #[cfg(test)] pub fn micro() -> Self { Prefix::Metric(-6) } #[cfg(test)] pub fn milli() -> Self { Prefix::Metric(-3) } #[cfg(test)] pub fn centi() -> Self { Prefix::Metric(-2) } #[cfg(test)] pub fn deci() -> Self { Prefix::Metric(-1) } #[cfg(test)] pub fn deca() -> Self { Prefix::Metric(1) } #[cfg(test)] pub fn hecto() -> Self { Prefix::Metric(2) } #[cfg(test)] pub fn kilo() -> Self { Prefix::Metric(3) } #[cfg(test)] pub fn mega() -> Self { Prefix::Metric(6) } #[cfg(test)] pub fn giga() -> Self { Prefix::Metric(9) } #[cfg(test)] pub fn tera() -> Self { Prefix::Metric(12) } #[cfg(test)] pub fn kibi() -> Self { Prefix::Binary(10) } #[cfg(test)] pub fn mebi() -> Self { Prefix::Binary(20) } #[cfg(test)] pub fn gibi() -> Self { Prefix::Binary(30) } pub fn is_none(&self) -> bool { match self { Prefix::Metric(0) => true, Prefix::Binary(0) => true, Prefix::Metric(_) => false, Prefix::Binary(_) => false, } } pub fn is_metric(&self) -> bool { matches!(self, Prefix::Metric(_)) } pub fn is_binary(&self) -> bool { matches!(self, Prefix::Binary(_)) } pub fn as_string_short(&self) -> String { match self { Prefix::Metric(-30) => "q".into(), Prefix::Metric(-27) => "r".into(), Prefix::Metric(-24) => "y".into(), Prefix::Metric(-21) => "z".into(), Prefix::Metric(-18) => "a".into(), Prefix::Metric(-15) => "f".into(), Prefix::Metric(-12) => "p".into(), Prefix::Metric(-9) => "n".into(), Prefix::Metric(-6) => "µ".into(), Prefix::Metric(-3) => "m".into(), Prefix::Metric(-2) => "c".into(), Prefix::Metric(-1) => "d".into(), Prefix::Metric(0) => "".into(), Prefix::Metric(1) => "da".into(), Prefix::Metric(2) => "h".into(), Prefix::Metric(3) => "k".into(), Prefix::Metric(6) => "M".into(), Prefix::Metric(9) => "G".into(), Prefix::Metric(12) => "T".into(), Prefix::Metric(15) => "P".into(), Prefix::Metric(18) => "E".into(), Prefix::Metric(21) => "Z".into(), Prefix::Metric(24) => "Y".into(), Prefix::Metric(27) => "R".into(), Prefix::Metric(30) => "Q".into(), Prefix::Metric(n) => format!("", n), Prefix::Binary(0) => "".into(), Prefix::Binary(10) => "Ki".into(), Prefix::Binary(20) => "Mi".into(), Prefix::Binary(30) => "Gi".into(), Prefix::Binary(40) => "Ti".into(), Prefix::Binary(50) => "Pi".into(), Prefix::Binary(60) => "Ei".into(), Prefix::Binary(70) => "Zi".into(), Prefix::Binary(80) => "Yi".into(), Prefix::Binary(n) => format!("", n), } } pub fn as_string_long(&self) -> String { match self { Prefix::Metric(-30) => "quecto".into(), Prefix::Metric(-27) => "ronto".into(), Prefix::Metric(-24) => "yocto".into(), Prefix::Metric(-21) => "zepto".into(), Prefix::Metric(-18) => "atto".into(), Prefix::Metric(-15) => "femto".into(), Prefix::Metric(-12) => "pico".into(), Prefix::Metric(-9) => "nano".into(), Prefix::Metric(-6) => "micro".into(), Prefix::Metric(-3) => "milli".into(), Prefix::Metric(-2) => "centi".into(), Prefix::Metric(-1) => "deci".into(), Prefix::Metric(0) => "".into(), Prefix::Metric(1) => "deca".into(), Prefix::Metric(2) => "hecto".into(), Prefix::Metric(3) => "kilo".into(), Prefix::Metric(6) => "mega".into(), Prefix::Metric(9) => "giga".into(), Prefix::Metric(12) => "tera".into(), Prefix::Metric(15) => "peta".into(), Prefix::Metric(18) => "exa".into(), Prefix::Metric(21) => "zetta".into(), Prefix::Metric(24) => "yotta".into(), Prefix::Metric(27) => "ronna".into(), Prefix::Metric(30) => "quetta".into(), Prefix::Metric(n) => format!("", n), Prefix::Binary(0) => "".into(), Prefix::Binary(10) => "kibi".into(), Prefix::Binary(20) => "mebi".into(), Prefix::Binary(30) => "gibi".into(), Prefix::Binary(40) => "tebi".into(), Prefix::Binary(50) => "pebi".into(), Prefix::Binary(60) => "exbi".into(), Prefix::Binary(70) => "zebi".into(), Prefix::Binary(80) => "yobi".into(), Prefix::Binary(n) => format!("", n), } } } numbat-1.11.0/src/prefix_parser.rs000064400000000000000000000402641046102023000152060ustar 00000000000000use std::collections::HashMap; use std::sync::OnceLock; use crate::span::Span; use crate::{name_resolution::NameResolutionError, prefix::Prefix}; static PREFIXES: OnceLock> = OnceLock::new(); #[derive(Debug, Clone, PartialEq)] pub enum PrefixParserResult { Identifier(String), /// Span, prefix, unit name in source (e.g. 'm'), full unit name (e.g. 'meter') UnitIdentifier(Span, Prefix, String, String), } type Result = std::result::Result; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AcceptsPrefix { pub short: bool, pub long: bool, } impl AcceptsPrefix { pub fn only_long() -> Self { Self { long: true, short: false, } } pub fn only_short() -> Self { Self { long: false, short: true, } } pub fn both() -> Self { Self { long: true, short: true, } } pub fn none() -> Self { Self { long: false, short: false, } } } #[derive(Debug, Clone)] struct UnitInfo { definition_span: Span, accepts_prefix: AcceptsPrefix, metric_prefixes: bool, binary_prefixes: bool, full_name: String, } #[derive(Debug, Clone)] pub struct PrefixParser { units: HashMap, // This is the exact same information as in the "units" hashmap, only faster to iterate over. // TODO: maybe use an external crate for this (e.g. indexmap?) units_vec: Vec<(String, UnitInfo)>, other_identifiers: HashMap, reserved_identifiers: &'static [&'static str], } impl PrefixParser { pub fn new() -> Self { Self { units: HashMap::new(), units_vec: Vec::new(), other_identifiers: HashMap::new(), reserved_identifiers: &["_", "ans"], } } fn prefixes() -> &'static [(&'static str, &'static [&'static str], Prefix)] { PREFIXES.get_or_init(|| { vec![ // Metric prefixes: ("quecto", &["q"], Prefix::Metric(-30)), ("ronto", &["r"], Prefix::Metric(-27)), ("yocto", &["y"], Prefix::Metric(-24)), ("zepto", &["z"], Prefix::Metric(-21)), ("atto", &["a"], Prefix::Metric(-18)), ("femto", &["f"], Prefix::Metric(-15)), ("pico", &["p"], Prefix::Metric(-12)), ("nano", &["n"], Prefix::Metric(-9)), ( "micro", &[ "µ", // Micro Sign (U+00B5) "μ", // Greek Small Letter Mu (U+03BC) "u", ], Prefix::Metric(-6), ), ("milli", &["m"], Prefix::Metric(-3)), ("centi", &["c"], Prefix::Metric(-2)), ("deci", &["d"], Prefix::Metric(-1)), ("deca", &["da"], Prefix::Metric(1)), ("hecto", &["h"], Prefix::Metric(2)), ("kilo", &["k"], Prefix::Metric(3)), ("mega", &["M"], Prefix::Metric(6)), ("giga", &["G"], Prefix::Metric(9)), ("tera", &["T"], Prefix::Metric(12)), ("peta", &["P"], Prefix::Metric(15)), ("exa", &["E"], Prefix::Metric(18)), ("zetta", &["Z"], Prefix::Metric(21)), ("yotta", &["Y"], Prefix::Metric(24)), ("ronna", &["R"], Prefix::Metric(27)), ("quetta", &["Q"], Prefix::Metric(30)), // Binary prefixes: ("kibi", &["Ki"], Prefix::Binary(10)), ("mebi", &["Mi"], Prefix::Binary(20)), ("gibi", &["Gi"], Prefix::Binary(30)), ("tebi", &["Ti"], Prefix::Binary(40)), ("pebi", &["Pi"], Prefix::Binary(50)), ("exbi", &["Ei"], Prefix::Binary(60)), ("zebi", &["Zi"], Prefix::Binary(70)), ("yobi", &["Yi"], Prefix::Binary(80)), // The following two prefixes are not yet approved by IEC as of 2023-02-16 // ("robi", "Ri", Prefix::Binary(90)), // ("quebi", "Qi", Prefix::Binary(100)), ] }) } fn identifier_clash_error( &self, name: &str, conflict_span: Span, original_span: Span, ) -> NameResolutionError { NameResolutionError::IdentifierClash { conflicting_identifier: name.to_string(), conflict_span, original_span, } } fn ensure_name_is_available( &self, name: &str, conflict_span: Span, clash_with_other_identifiers: bool, ) -> Result<()> { if self.reserved_identifiers.contains(&name) { return Err(NameResolutionError::ReservedIdentifier(conflict_span)); } if clash_with_other_identifiers { if let Some(original_span) = self.other_identifiers.get(name) { return Err(self.identifier_clash_error(name, conflict_span, *original_span)); } } match self.parse(name) { PrefixParserResult::Identifier(_) => Ok(()), PrefixParserResult::UnitIdentifier(original_span, _, _, _) => { Err(self.identifier_clash_error(name, conflict_span, original_span)) } } } pub fn add_unit( &mut self, unit_name: &str, accepts_prefix: AcceptsPrefix, metric: bool, binary: bool, full_name: &str, definition_span: Span, ) -> Result<()> { self.ensure_name_is_available(unit_name, definition_span, true)?; for (prefix_long, prefixes_short, prefix) in Self::prefixes() { if !(prefix.is_metric() && metric || prefix.is_binary() && binary) { continue; } if accepts_prefix.long { self.ensure_name_is_available( &format!("{}{}", prefix_long, unit_name), definition_span, true, )?; } if accepts_prefix.short { for prefix_short in *prefixes_short { self.ensure_name_is_available( &format!("{}{}", prefix_short, unit_name), definition_span, true, )?; } } } let unit_info = UnitInfo { definition_span, accepts_prefix, metric_prefixes: metric, binary_prefixes: binary, full_name: full_name.into(), }; self.units.insert(unit_name.into(), unit_info.clone()); self.units_vec.push((unit_name.into(), unit_info)); Ok(()) } pub fn add_other_identifier(&mut self, identifier: &str, definition_span: Span) -> Result<()> { self.ensure_name_is_available(identifier, definition_span, false)?; self.other_identifiers .insert(identifier.into(), definition_span); Ok(()) } pub fn parse(&self, input: &str) -> PrefixParserResult { if let Some(info) = self.units.get(input) { return PrefixParserResult::UnitIdentifier( info.definition_span, Prefix::none(), input.into(), info.full_name.clone(), ); } for (unit_name, info) in &self.units_vec { if !input.ends_with(unit_name.as_str()) { continue; } for (prefix_long, prefixes_short, prefix) in Self::prefixes() { let is_metric = prefix.is_metric(); let is_binary = prefix.is_binary(); if info.accepts_prefix.long && (is_metric && info.metric_prefixes || is_binary && info.binary_prefixes) && input.starts_with(prefix_long) && &input[prefix_long.len()..] == unit_name { return PrefixParserResult::UnitIdentifier( info.definition_span, *prefix, unit_name.to_string(), info.full_name.clone(), ); } if info.accepts_prefix.short && (is_metric && info.metric_prefixes || is_binary && info.binary_prefixes) && prefixes_short.iter().any(|prefix_short| { input.starts_with(prefix_short) && &input[prefix_short.len()..] == unit_name }) { return PrefixParserResult::UnitIdentifier( info.definition_span, *prefix, unit_name.to_string(), info.full_name.clone(), ); } } } PrefixParserResult::Identifier(input.into()) } } #[cfg(test)] mod tests { use super::*; #[test] fn basic() { let mut prefix_parser = PrefixParser::new(); prefix_parser .add_unit( "meter", AcceptsPrefix::only_long(), true, false, "meter", Span::dummy(), ) .unwrap(); prefix_parser .add_unit( "m", AcceptsPrefix::only_short(), true, false, "meter", Span::dummy(), ) .unwrap(); prefix_parser .add_unit( "byte", AcceptsPrefix::only_long(), true, true, "byte", Span::dummy(), ) .unwrap(); prefix_parser .add_unit( "B", AcceptsPrefix::only_short(), true, true, "byte", Span::dummy(), ) .unwrap(); prefix_parser .add_unit( "me", AcceptsPrefix::only_short(), false, false, "me", Span::dummy(), ) .unwrap(); assert_eq!( prefix_parser.parse("meter"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::none(), "meter".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("m"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::none(), "m".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("byte"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::none(), "byte".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("B"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::none(), "B".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("me"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::none(), "me".into(), "me".into() ) ); assert_eq!( prefix_parser.parse("kilometer"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::kilo(), "meter".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("millimeter"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::milli(), "meter".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("kilobyte"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::kilo(), "byte".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("kibibyte"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::kibi(), "byte".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("mebibyte"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::mebi(), "byte".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("km"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::kilo(), "m".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("mm"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::milli(), "m".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("µm"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::micro(), "m".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("μm"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::micro(), "m".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("um"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::micro(), "m".into(), "meter".into() ) ); assert_eq!( prefix_parser.parse("kB"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::kilo(), "B".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("MB"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::mega(), "B".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("KiB"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::kibi(), "B".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("MiB"), PrefixParserResult::UnitIdentifier( Span::dummy(), Prefix::mebi(), "B".into(), "byte".into() ) ); assert_eq!( prefix_parser.parse("kilom"), PrefixParserResult::Identifier("kilom".into()) ); assert_eq!( prefix_parser.parse("kilome"), PrefixParserResult::Identifier("kilome".into()) ); assert_eq!( prefix_parser.parse("kme"), PrefixParserResult::Identifier("kme".into()) ); assert_eq!( prefix_parser.parse("kilomete"), PrefixParserResult::Identifier("kilomete".into()) ); assert_eq!( prefix_parser.parse("kilometerr"), PrefixParserResult::Identifier("kilometerr".into()) ); assert_eq!( prefix_parser.parse("foometer"), PrefixParserResult::Identifier("foometer".into()) ); assert_eq!( prefix_parser.parse("kibimeter"), PrefixParserResult::Identifier("kibimeter".into()) ); assert_eq!( prefix_parser.parse("Kim"), PrefixParserResult::Identifier("Kim".into()) ); } } numbat-1.11.0/src/prefix_transformer.rs000064400000000000000000000211571046102023000162540ustar 00000000000000use crate::{ ast::{Expression, Statement, StringPart}, decorator::{self, Decorator}, name_resolution::NameResolutionError, prefix_parser::{PrefixParser, PrefixParserResult}, span::Span, }; type Result = std::result::Result; #[derive(Debug, Clone)] pub(crate) struct Transformer { pub prefix_parser: PrefixParser, pub variable_names: Vec, pub function_names: Vec, pub unit_names: Vec>, pub dimension_names: Vec, } impl Transformer { pub fn new() -> Self { Self { prefix_parser: PrefixParser::new(), variable_names: vec![], function_names: vec![], unit_names: vec![], dimension_names: vec![], } } fn transform_expression(&self, expression: Expression) -> Expression { match expression { expr @ Expression::Scalar(..) => expr, Expression::Identifier(span, identifier) => { if let PrefixParserResult::UnitIdentifier( _definition_span, prefix, unit_name, full_name, ) = self.prefix_parser.parse(&identifier) { Expression::UnitIdentifier(span, prefix, unit_name, full_name) } else { Expression::Identifier(span, identifier) } } Expression::UnitIdentifier(_, _, _, _) => { unreachable!("Prefixed identifiers should not exist prior to this stage") } Expression::UnaryOperator { op, expr, span_op } => Expression::UnaryOperator { op, expr: Box::new(self.transform_expression(*expr)), span_op, }, Expression::BinaryOperator { op, lhs, rhs, span_op, } => Expression::BinaryOperator { op, lhs: Box::new(self.transform_expression(*lhs)), rhs: Box::new(self.transform_expression(*rhs)), span_op, }, Expression::FunctionCall(span, full_span, name, args) => Expression::FunctionCall( span, full_span, name, args.into_iter() .map(|arg| self.transform_expression(arg)) .collect(), ), expr @ Expression::Boolean(_, _) => expr, Expression::Condition(span, condition, then, else_) => Expression::Condition( span, Box::new(self.transform_expression(*condition)), Box::new(self.transform_expression(*then)), Box::new(self.transform_expression(*else_)), ), Expression::String(span, parts) => Expression::String( span, parts .into_iter() .map(|p| match p { f @ StringPart::Fixed(_) => f, StringPart::Interpolation(span, expr) => StringPart::Interpolation( span, Box::new(self.transform_expression(*expr)), ), }) .collect(), ), } } fn has_decorator(decorators: &[Decorator], decorator: Decorator) -> bool { decorators.iter().any(|d| d == &decorator) } pub(crate) fn register_name_and_aliases( &mut self, name: &String, decorators: &[Decorator], conflict_span: Span, ) -> Result<()> { let mut unit_names = vec![]; let metric_prefixes = Self::has_decorator(decorators, Decorator::MetricPrefixes); let binary_prefixes = Self::has_decorator(decorators, Decorator::BinaryPrefixes); for (alias, accepts_prefix) in decorator::name_and_aliases(name, decorators) { self.prefix_parser.add_unit( alias, accepts_prefix, metric_prefixes, binary_prefixes, name, conflict_span, )?; unit_names.push(alias.to_string()); } unit_names.sort(); self.unit_names.push(unit_names); Ok(()) } fn transform_statement(&mut self, statement: Statement) -> Result { Ok(match statement { Statement::Expression(expr) => Statement::Expression(self.transform_expression(expr)), Statement::DefineBaseUnit(span, name, dexpr, decorators) => { self.register_name_and_aliases(&name, &decorators, span)?; Statement::DefineBaseUnit(span, name, dexpr, decorators) } Statement::DefineDerivedUnit { identifier_span, identifier, expr, type_annotation_span, type_annotation, decorators, } => { self.register_name_and_aliases(&identifier, &decorators, identifier_span)?; Statement::DefineDerivedUnit { identifier_span, identifier, expr: self.transform_expression(expr), type_annotation_span, type_annotation, decorators, } } Statement::DefineVariable { identifier_span, identifier, expr, type_annotation, decorators, } => { for (name, _) in decorator::name_and_aliases(&identifier, &decorators) { self.variable_names.push(name.clone()); } self.prefix_parser .add_other_identifier(&identifier, identifier_span)?; Statement::DefineVariable { identifier_span, identifier, expr: self.transform_expression(expr), type_annotation, decorators, } } Statement::DefineFunction { function_name_span, function_name, type_parameters, parameters, body, return_type_annotation_span: return_type_span, return_type_annotation, } => { self.function_names.push(function_name.clone()); self.prefix_parser .add_other_identifier(&function_name, function_name_span)?; // We create a clone of the full transformer for the purpose // of checking/transforming the function body. The reason for this // is that we don't want the parameter names to pollute the global // namespace. But we need to register parameter names as identifiers // because they could otherwise shadow global identifiers: // // fn foo(t: Time) -> Time = t # not okay: shadows 't' for ton // let mut fn_body_transformer = self.clone(); for (param_span, param, _, _) in ¶meters { fn_body_transformer .prefix_parser .add_other_identifier(param, *param_span)?; } Statement::DefineFunction { function_name_span, function_name, type_parameters, parameters, body: body.map(|expr| self.transform_expression(expr)), return_type_annotation_span: return_type_span, return_type_annotation, } } Statement::DefineDimension(name, dexprs) => { self.dimension_names.push(name.clone()); Statement::DefineDimension(name, dexprs) } Statement::ProcedureCall(span, procedure, args) => Statement::ProcedureCall( span, procedure, args.into_iter() .map(|arg| self.transform_expression(arg)) .collect(), ), statement @ Statement::ModuleImport(_, _) => statement, }) } pub fn transform( &mut self, statements: impl IntoIterator, ) -> Result> { statements .into_iter() .map(|statement| self.transform_statement(statement)) .collect() } } numbat-1.11.0/src/pretty_print.rs000064400000000000000000000005371046102023000150770ustar 00000000000000use crate::markup::Markup; pub trait PrettyPrint { fn pretty_print(&self) -> Markup; } impl PrettyPrint for bool { fn pretty_print(&self) -> Markup { crate::markup::keyword(if *self { "true" } else { "false" }) } } impl PrettyPrint for String { fn pretty_print(&self) -> Markup { crate::markup::string(self) } } numbat-1.11.0/src/product.rs000064400000000000000000000275621046102023000140230ustar 00000000000000use std::{ fmt::Display, ops::{Div, Mul}, }; use crate::arithmetic::{Exponent, Power}; use crate::markup::{self as m, Formatter, PlainTextFormatter}; use itertools::Itertools; use num_rational::Ratio; use num_traits::Signed; pub trait Canonicalize { type MergeKey: PartialEq; fn merge_key(&self) -> Self::MergeKey; fn merge(self, other: Self) -> Self; fn is_trivial(&self) -> bool; } #[derive(Debug, Clone)] pub struct Product { factors: Vec, } impl Product { /// The last argument controls how the factor is formated. /// /// By default it is with `TypeIdentifier`, but `Unit` can be used too. pub fn pretty_print_with( &self, get_exponent: GetExponent, times_separator: char, over_separator: char, separator_padding: bool, format_type: Option, ) -> m::Markup where GetExponent: Fn(&Factor) -> Exponent, { let format_type = format_type.unwrap_or(m::FormatType::TypeIdentifier); let separator_padding = if separator_padding { m::space() } else { m::empty() }; let to_string = |fs: &[Factor]| -> m::Markup { let mut result = m::empty(); let num_factors = fs.len(); for (i, factor) in fs.iter().enumerate() { result = result + m::Markup::from(m::FormattedString( m::OutputType::Normal, format_type, factor.to_string(), )) + if i == num_factors - 1 { m::empty() } else { separator_padding.clone() + m::operator(times_separator.to_string()) + separator_padding.clone() }; } result }; let factors_positive: Vec<_> = self .iter() .filter(|f| get_exponent(*f).is_positive()) .cloned() .collect(); let factors_negative: Vec<_> = self .iter() .filter(|f| !get_exponent(*f).is_positive()) .cloned() .collect(); match (&factors_positive[..], &factors_negative[..]) { (&[], &[]) => m::empty(), (&[], negative) => to_string(negative), (positive, &[]) => to_string(positive), (positive, [single_negative]) => { to_string(positive) + separator_padding.clone() + m::operator(over_separator.to_string()) + separator_padding.clone() + to_string(&[single_negative.clone().invert()]) } (positive, negative) => { to_string(positive) + separator_padding.clone() + m::operator(over_separator.to_string()) + separator_padding.clone() + m::operator("(") + to_string(&negative.iter().map(|f| f.clone().invert()).collect_vec()) + m::operator(")") } } } pub fn as_string( &self, get_exponent: GetExponent, times_separator: char, over_separator: char, separator_padding: bool, ) -> String where GetExponent: Fn(&Factor) -> Exponent, { PlainTextFormatter {}.format( &self.pretty_print_with( get_exponent, times_separator, over_separator, separator_padding, None, ), false, ) } } impl Product { pub fn unity() -> Self { Self::from_factors([]) } pub fn from_factors(factors: impl IntoIterator) -> Self { Self::from_vec(factors.into_iter().collect()) } pub fn from_factor(factor: Factor) -> Self { Self { factors: vec![factor], } } fn from_vec(factors: Vec) -> Self { let mut product = Self { factors }; product.automated_canonicalize(); product } pub fn iter(&self) -> ProductIter { ProductIter { inner: self.factors.iter(), } } #[cfg(test)] fn into_vec(self) -> Vec { self.factors } fn automated_canonicalize(&mut self) { if CANONICALIZE { self.canonicalize(); } } pub fn canonicalize(&mut self) { self.factors.sort_unstable(); self.factors = self .factors .iter() .cloned() .group_by(|f1| f1.merge_key()) .into_iter() .map(|(_, group)| { group .reduce(|acc, item| acc.merge(item)) .expect("non zero group") }) .filter(|factor| !factor.is_trivial()) .collect(); } pub fn canonicalized(&self) -> Self { let mut result = self.clone(); result.canonicalize(); result } } impl Mul for Product { type Output = Self; fn mul(mut self, mut other: Self) -> Self { self.factors.append(&mut other.factors); Self::from_vec(self.factors) } } impl Power for Product { fn power(self, exp: Exponent) -> Self { Product::from_factors(self.factors.into_iter().map(|f| f.power(exp))) } } impl Product { pub fn invert(self) -> Self { self.powi(-1) } pub fn powi(self, exp: i128) -> Self { self.power(Ratio::from_integer(exp)) } } impl Div for Product { type Output = Self; fn div(self, other: Self) -> Self { #[allow(clippy::suspicious_arithmetic_impl)] let mut result = self * other.invert(); result.automated_canonicalize(); result } } impl PartialEq for Product { fn eq(&self, other: &Self) -> bool { self.canonicalized().factors == other.canonicalized().factors } } impl Eq for Product { } impl IntoIterator for Product { type IntoIter = ProductIntoIter; type Item = Factor; fn into_iter(self) -> Self::IntoIter { ProductIntoIter { inner: self.factors.into_iter(), } } } impl std::iter::Product for Product { fn product(iter: I) -> Self where I: Iterator, { Self::from_factors(iter) } } impl std::iter::Product for Product { fn product(iter: I) -> Self where I: Iterator, { iter.fold(Product::unity(), |acc, prod| acc * prod) } } pub struct ProductIter<'a, Factor> { inner: std::slice::Iter<'a, Factor>, } impl<'a, Factor> Iterator for ProductIter<'a, Factor> { type Item = &'a Factor; fn next(&mut self) -> Option { self.inner.next() } } pub struct ProductIntoIter { inner: std::vec::IntoIter, } impl Iterator for ProductIntoIter { type Item = Factor; fn next(&mut self) -> Option { self.inner.next() } } #[cfg(test)] mod tests { use super::*; use crate::arithmetic::Rational; #[cfg(test)] impl Canonicalize for i32 { type MergeKey = (); fn merge_key(&self) -> Self::MergeKey { // merge everything } fn merge(self, other: Self) -> Self { self * other } fn is_trivial(&self) -> bool { *self == 1 } } #[test] fn multiply() { let product1 = Product::::from_factors([5, 2, 3]); let product2 = Product::::from_factors([6, 8]); let result = product1 * product2; assert_eq!( result.into_iter().collect::>().as_slice(), [5, 2, 3, 6, 8] ); } #[test] fn multiply_canonicalize() { use crate::arithmetic::Rational; let product1 = Product::::from_factors([ TestUnit("meter".into(), Rational::from_integer(1)), TestUnit("second".into(), Rational::from_integer(1)), ]); let product2 = Product::from_factor(TestUnit("meter".into(), Rational::from_integer(2))); let result = product1 * product2; assert_eq!( result.into_vec(), &[ TestUnit("meter".into(), Rational::from_integer(3)), TestUnit("second".into(), Rational::from_integer(1)) ] ); } #[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct TestUnit(String, Exponent); #[cfg(test)] impl Canonicalize for TestUnit { type MergeKey = String; fn merge_key(&self) -> Self::MergeKey { self.0.clone() } fn merge(self, other: Self) -> Self { TestUnit(self.0, self.1 + other.1) } fn is_trivial(&self) -> bool { use num_traits::Zero; self.1 == Rational::zero() } } #[cfg(test)] impl Power for TestUnit { fn power(self, e: Exponent) -> Self { TestUnit(self.0, self.1 * e) } } #[test] fn power() { let product = Product::::from_factors([ TestUnit("meter".into(), Rational::from_integer(1)), TestUnit("second".into(), Rational::from_integer(-2)), ]); let result = product.powi(3); assert_eq!( result.into_vec(), &[ TestUnit("meter".into(), Rational::from_integer(3)), TestUnit("second".into(), Rational::from_integer(-6)) ] ); } #[test] fn divide() { let product1 = Product::::from_factors([ TestUnit("meter".into(), Rational::from_integer(1)), TestUnit("second".into(), Rational::from_integer(1)), ]); let product2 = Product::from_factor(TestUnit("second".into(), Rational::from_integer(1))); let result = product1 / product2; assert_eq!( result.into_vec(), &[ TestUnit("meter".into(), Rational::from_integer(1)), TestUnit("second".into(), Rational::from_integer(1)), TestUnit("second".into(), Rational::from_integer(-1)) ] ); } #[test] fn iter() { let product = Product::::from_factors([5, 2, 3]); let mut iter = product.iter(); assert_eq!(iter.next(), Some(&5)); assert_eq!(iter.next(), Some(&2)); assert_eq!(iter.next(), Some(&3)); assert_eq!(iter.next(), None); assert_eq!(iter.next(), None); } #[test] fn canonicalize() { let mut product = Product::::from_factors([5, 2, 3]); product.canonicalize(); assert_eq!(product.into_iter().collect::>().as_slice(), [30]); } } numbat-1.11.0/src/quantity.rs000064400000000000000000000466311046102023000142170ustar 00000000000000use crate::arithmetic::{Exponent, Power, Rational}; use crate::number::Number; use crate::pretty_print::PrettyPrint; use crate::unit::{is_multiple_of, Unit, UnitFactor}; use itertools::Itertools; use num_rational::Ratio; use num_traits::{FromPrimitive, Zero}; use thiserror::Error; #[derive(Clone, Debug, Error, PartialEq, Eq)] pub enum QuantityError { #[error("Conversion error: unit '{0}' can not be converted to '{1}'")] IncompatibleUnits(Unit, Unit), // TODO: this can currently be triggered if there are multiple base units for the same dimension (no way to convert between them) #[error("Non-rational exponent")] NonRationalExponent, } pub type Result = std::result::Result; #[derive(Debug, Clone)] pub struct Quantity { value: Number, unit: Unit, } impl Quantity { pub fn new(value: Number, unit: Unit) -> Self { Quantity { value, unit } } pub fn new_f64(value: f64, unit: Unit) -> Self { Quantity { value: Number::from_f64(value), unit, } } pub fn from_scalar(value: f64) -> Quantity { Quantity::new_f64(value, Unit::scalar()) } pub fn from_unit(unit: Unit) -> Quantity { Quantity::new_f64(1.0, unit) } pub fn unit(&self) -> &Unit { &self.unit } pub fn is_zero(&self) -> bool { self.value.to_f64() == 0.0 } pub fn to_base_unit_representation(&self) -> Quantity { let (unit, factor) = self.unit.to_base_unit_representation(); Quantity::new(self.value * factor, unit) } pub fn convert_to(&self, target_unit: &Unit) -> Result { if &self.unit == target_unit { Ok(Quantity::new(self.value, target_unit.clone())) } else { // Remove common unit factors to reduce unnecessary conversion procedures // For example: when converting from km/hour to mile/hour, there is no need // to also perform the hour->second conversion, which would be needed, as // we go back to base units for now. Removing common factors is just one // heuristic, but it would be better to solve this in a more general way. // For more details on this problem, see [1]. // // [1] https://github.com/sharkdp/numbat/issues/118. let mut common_unit_factors = Unit::scalar(); let target_unit_canonicalized = target_unit.canonicalized(); for factor in self.unit.canonicalized().iter() { if let Some(other_factor) = target_unit_canonicalized .iter() .find(|&f| factor.prefix == f.prefix && factor.unit_id == f.unit_id) { if factor.exponent > Ratio::zero() && other_factor.exponent > Ratio::zero() { common_unit_factors = common_unit_factors * Unit::from_factor(UnitFactor { exponent: std::cmp::min(factor.exponent, other_factor.exponent), ..factor.clone() }); } else if factor.exponent < Ratio::zero() && other_factor.exponent < Ratio::zero() { common_unit_factors = common_unit_factors * Unit::from_factor(UnitFactor { exponent: std::cmp::max(factor.exponent, other_factor.exponent), ..factor.clone() }); } } } let target_unit_reduced = (target_unit.clone() / common_unit_factors.clone()).canonicalized(); let own_unit_reduced = (self.unit.clone() / common_unit_factors.clone()).canonicalized(); let (target_base_unit_representation, factor) = target_unit_reduced.to_base_unit_representation(); let quantity_base_unit_representation = (self.clone() / Quantity::from_unit(common_unit_factors)) .to_base_unit_representation(); let own_base_unit_representation = own_unit_reduced.to_base_unit_representation().0; if own_base_unit_representation == target_base_unit_representation { Ok(Quantity::new( *quantity_base_unit_representation.unsafe_value() / factor, target_unit.clone(), )) } else { // TODO: can this even be triggered? replace by an assertion? Err(QuantityError::IncompatibleUnits( self.unit.clone(), target_unit.clone(), )) } } } pub fn full_simplify(&self) -> Self { // Heuristic 1 if let Ok(scalar_result) = self.convert_to(&Unit::scalar()) { return scalar_result; } // Heuristic 2 let unit = self.unit.canonicalized(); if unit.iter().count() > 1 { for factor in unit.iter() { let mut factor = factor.clone(); factor.exponent = Exponent::from_integer(1); let factor_unit = Unit::from_factor(factor); if let Some(alpha) = is_multiple_of(&unit, &factor_unit) { if alpha.is_integer() { let simplified_unit = factor_unit.power(alpha); if let Ok(q) = self.convert_to(&simplified_unit) { return q; } } } } } // Heuristic 3 let removed_exponent = |u: &UnitFactor| { let base_unit = u.unit_id.base_unit_and_factor().0; if let Some(first_factor) = base_unit.into_iter().next() { first_factor.exponent } else { Ratio::from_integer(1) } }; let mut factor = Number::from_f64(1.0); let mut simplified_unit = Unit::scalar(); for (_, group) in &self .unit .canonicalized() .iter() .group_by(|f| f.unit_id.sort_key()) { let group_as_unit = Unit::from_factors(group.cloned()); let group_representative = group_as_unit .iter() .max_by(|&f1, &f2| { // TODO: describe this heuristic (f1.unit_id.is_base().cmp(&f2.unit_id.is_base())) .then(f1.exponent.cmp(&f2.exponent)) }) .expect("At least one unit factor in the group"); let target_unit = if group_as_unit.to_base_unit_representation().0.is_scalar() { // If the group-representative is convertible to a scalar, // use 'scalar' as the target unit instead. This allows us // to simplify, for example, '3% · kg' to '0.03 kg'. Unit::scalar() } else { let exponent = group_as_unit .iter() .map(|f| { f.exponent * removed_exponent(f) / removed_exponent(group_representative) }) .sum(); Unit::from_factor(UnitFactor { exponent, ..group_representative.clone() }) }; let converted = Quantity::from_unit(group_as_unit) .convert_to(&target_unit) .unwrap(); simplified_unit = simplified_unit * target_unit; factor = factor * converted.value; } simplified_unit.canonicalize(); Quantity::new(self.value * factor, simplified_unit) } pub fn as_scalar(&self) -> Result { Ok(self.convert_to(&Unit::scalar())?.value) } pub fn unsafe_value(&self) -> &Number { &self.value } pub fn power(self, exp: Quantity) -> Result { let exponent_as_scalar = exp.as_scalar()?.to_f64(); Ok(Quantity::new_f64( self.value.to_f64().powf(exponent_as_scalar), self.unit.power( Rational::from_f64(exponent_as_scalar).ok_or(QuantityError::NonRationalExponent)?, ), )) } pub fn checked_div(self, other: Self) -> Option { if other.is_zero() { None } else { Some(self / other) } } } impl From<&Number> for Quantity { fn from(n: &Number) -> Self { Quantity::from_scalar(n.to_f64()) } } impl std::ops::Add for &Quantity { type Output = Result; fn add(self, rhs: Self) -> Self::Output { Ok(Quantity { value: self.value + rhs.convert_to(&self.unit)?.value, unit: self.unit.clone(), }) } } impl std::ops::Sub for &Quantity { type Output = Result; fn sub(self, rhs: Self) -> Self::Output { Ok(Quantity { value: self.value - rhs.convert_to(&self.unit)?.value, unit: self.unit.clone(), }) } } impl std::ops::Mul for Quantity { type Output = Quantity; fn mul(self, rhs: Self) -> Self::Output { Quantity { value: self.value * rhs.value, unit: self.unit * rhs.unit, } } } impl std::ops::Div for Quantity { type Output = Quantity; fn div(self, rhs: Self) -> Self::Output { Quantity { value: self.value / rhs.value, unit: self.unit / rhs.unit, } } } impl std::ops::Neg for Quantity { type Output = Quantity; fn neg(self) -> Self::Output { Quantity { value: -self.value, unit: self.unit, } } } impl PartialEq for Quantity { fn eq(&self, other: &Self) -> bool { if let Ok(other_converted) = other.convert_to(self.unit()) { self.value == other_converted.value } else { false } } } impl Eq for Quantity {} impl PartialOrd for Quantity { fn partial_cmp(&self, other: &Self) -> Option { let other_converted = other.convert_to(self.unit()).ok()?; self.value.partial_cmp(&other_converted.value) } } impl PrettyPrint for Quantity { fn pretty_print(&self) -> crate::markup::Markup { use crate::markup; let formatted_number = self.unsafe_value().pretty_print(); let unit_str = format!("{}", self.unit()); markup::value(formatted_number) + if unit_str == "°" { markup::empty() } else { markup::space() } + markup::unit(unit_str) } } impl std::fmt::Display for Quantity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use crate::markup::{Formatter, PlainTextFormatter}; let markup = self.pretty_print(); let formatter = PlainTextFormatter {}; write!(f, "{}", formatter.format(&markup, false).trim()) } } #[cfg(test)] mod tests { use crate::{prefix::Prefix, prefix_parser::AcceptsPrefix, unit::CanonicalName}; use super::*; #[test] fn conversion_trivial() { let meter = Unit::meter(); let second = Unit::second(); let length = Quantity::new_f64(2.0, meter.clone()); assert!(length.convert_to(&meter).is_ok()); assert!(length.convert_to(&second).is_err()); assert!(length.convert_to(&Unit::scalar()).is_err()); } #[test] fn conversion_basic() { use approx::assert_relative_eq; let meter = Unit::meter(); let foot = Unit::new_derived( "foot", CanonicalName::new("ft", AcceptsPrefix::none()), Number::from_f64(0.3048), meter.clone(), ); let length = Quantity::new_f64(2.0, meter.clone()); let length_in_foot = length.convert_to(&foot).expect("conversion succeeds"); assert_eq!(length_in_foot.unsafe_value().to_f64(), 2.0 / 0.3048); let length_converted_back_to_meter = length_in_foot .convert_to(&meter) .expect("conversion succeeds"); assert_relative_eq!( length_converted_back_to_meter.unsafe_value().to_f64(), 2.0, epsilon = 1e-6 ); } #[test] fn prefixes() { use crate::prefix::Prefix; use approx::assert_relative_eq; let meter = Unit::meter(); let centimeter = Unit::meter().with_prefix(Prefix::centi()); let length = Quantity::new_f64(2.5, meter.clone()); { let length_in_centimeter = length.convert_to(¢imeter).expect("conversion succeeds"); assert_relative_eq!( length_in_centimeter.unsafe_value().to_f64(), 250.0, epsilon = 1e-6 ); let length_converted_back_to_meter = length_in_centimeter .convert_to(&meter) .expect("conversion succeeds"); assert_relative_eq!( length_converted_back_to_meter.unsafe_value().to_f64(), 2.5, epsilon = 1e-6 ); } { let volume = length .power(Quantity::from_scalar(3.0)) .expect("exponent is scalar"); let volume_in_centimeter3 = volume .convert_to(¢imeter.powi(3)) .expect("conversion succeeds"); assert_relative_eq!( volume_in_centimeter3.unsafe_value().to_f64(), 15_625_000.0, epsilon = 1e-6 ); } } #[test] fn full_simplify_basic() { let q = Quantity::new_f64(2.0, Unit::meter() / Unit::second()); assert_eq!(q.full_simplify(), q); } #[test] fn full_simplify_convertible_to_scalar() { { let q = Quantity::new_f64(2.0, Unit::meter() / Unit::millimeter()); assert_eq!(q.full_simplify(), Quantity::from_scalar(2000.0)); } { let q = Quantity::new_f64(2.0, Unit::kilometer() / Unit::millimeter()); assert_eq!(q.full_simplify(), Quantity::from_scalar(2000000.0)); } { let q = Quantity::new_f64(2.0, Unit::meter() / Unit::centimeter() * Unit::second()); assert_eq!( q.full_simplify(), Quantity::new_f64(2.0 * 100.0, Unit::second()) ); } { let q = Quantity::new_f64(1.0, Unit::kph() / (Unit::kilometer() / Unit::hour())); assert_eq!(q.full_simplify(), Quantity::from_scalar(1.0)); } } #[test] fn full_simplify_unit_rearrangements() { { let q = Quantity::new_f64(2.0, Unit::meter() * Unit::second() * Unit::meter()); let expected = Quantity::new_f64(2.0, Unit::meter().powi(2) * Unit::second()); assert_eq!(q.full_simplify(), expected); } { let q = Quantity::new_f64(2.0, Unit::kilometer() / Unit::millimeter()); assert_eq!(q.full_simplify(), Quantity::from_scalar(2000000.0)); } { let q = Quantity::new_f64(1.0, Unit::meter() * Unit::gram() / Unit::centimeter()); assert_eq!(q.full_simplify(), Quantity::new_f64(100.0, Unit::gram())); } } #[test] fn full_simplify_scalarlike_units() { { let q = Quantity::new_f64(3.0, Unit::percent() * Unit::kilogram()); assert_eq!(q.full_simplify(), Quantity::new_f64(0.03, Unit::kilogram())); } } #[test] fn full_simplify_complex() { { let q = Quantity::new_f64(5.0, Unit::second() * Unit::millimeter() / Unit::meter()); let expected = Quantity::new_f64(0.005, Unit::second()); assert_eq!(q.full_simplify(), expected); } { let q = Quantity::new_f64( 5.0, Unit::bit().with_prefix(Prefix::mega()) / Unit::second() * Unit::hour(), ); let expected = Quantity::new_f64(18000.0, Unit::bit().with_prefix(Prefix::mega())); assert_eq!(q.full_simplify(), expected); } { let q = Quantity::new_f64(5.0, Unit::centimeter() * Unit::meter()); let expected = Quantity::new_f64(500.0, Unit::centimeter().powi(2)); assert_eq!(q.full_simplify(), expected); } { let q = Quantity::new_f64(5.0, Unit::meter() * Unit::centimeter()); let expected = Quantity::new_f64(500.0, Unit::centimeter().powi(2)); assert_eq!(q.full_simplify(), expected); } { let q = Quantity::new_f64(1.0, Unit::hertz() / Unit::second()); let expected = Quantity::new_f64(1.0, Unit::second().powi(-2)); assert_eq!(q.full_simplify(), expected); } { let q = Quantity::new_f64(1.0, Unit::gallon() / Unit::inch()); let expected = Quantity::new_f64(231.0, Unit::inch().powi(2)); assert_eq!(q.full_simplify(), expected); } { let q = Quantity::new_f64(1.0, Unit::gallon() / Unit::inch().powi(2)); let expected = Quantity::new_f64(231.0, Unit::inch()); assert_eq!(q.full_simplify(), expected); } } #[test] fn si_compliant_pretty_printing() { // See: https://en.wikipedia.org/wiki/International_System_of_Units // -> Unit symbols and the value of quantities // The value of a quantity is written as a number followed by a space // (representing a multiplication sign) and a unit symbol; e.g., 2.21 kg, // 7.3×10² m², 22 K. assert_eq!( Quantity::new_f64(2.21, Unit::kilogram()).to_string(), "2.21 kg" ); assert_eq!(Quantity::new_f64(22.0, Unit::kelvin()).to_string(), "22 K"); // Exceptions are the symbols for plane angular degrees, minutes, and // seconds (°, ′, and ″), which are placed immediately after the // number with no intervening space. assert_eq!(Quantity::new_f64(90.0, Unit::degree()).to_string(), "90°"); // A prefix is part of the unit, and its symbol is prepended to the // unit symbol without a separator (e.g., k in km, M in MPa, G in GHz). // Compound prefixes are not allowed. assert_eq!( Quantity::new_f64(1.0, Unit::hertz().with_prefix(Prefix::giga())).to_string(), "1 GHz" ); // Symbols for derived units formed by multiplication are joined with a // centre dot (·) or a non-breaking space; e.g., N·m or N m. assert_eq!( Quantity::new_f64(1.0, Unit::newton() * Unit::meter()).to_string(), "1 N·m" ); // Symbols for derived units formed by division are joined with a solidus // (/), or given as a negative exponent. E.g., the "metre per second" can // be written m/s, m s^(−1), m·s^(−1), or m/s. Only one solidus should // be used; e.g., kg/(m·s²) and kg·m^(−1)·s^(−2) are acceptable, but // kg/m/s² is ambiguous and unacceptable. assert_eq!( Quantity::new_f64(1.0, Unit::meter() / Unit::meter()).to_string(), "1 m/m" ); assert_eq!( Quantity::new_f64( 1.0, Unit::kilogram() / (Unit::meter() * Unit::second().powi(2)) ) .to_string(), "1 kg/(m·s²)" ); } } numbat-1.11.0/src/registry.rs000064400000000000000000000122761046102023000142070ustar 00000000000000use std::{collections::HashMap, fmt::Display}; use itertools::Itertools; use num_traits::Zero; use thiserror::Error; use crate::{ arithmetic::{pretty_exponent, Exponent, Power, Rational}, pretty_print::PrettyPrint, product::{Canonicalize, Product}, suggestion, }; #[derive(Clone, Error, Debug, PartialEq, Eq)] pub enum RegistryError { #[error("Entry '{0}' exists already.")] EntryExists(String), #[error("Unknown entry '{0}'.")] UnknownEntry(String, Option), } pub type Result = std::result::Result; pub type BaseEntry = String; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BaseIndex(isize); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct BaseRepresentationFactor(pub BaseEntry, pub Exponent); impl Display for BaseRepresentationFactor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.0, pretty_exponent(&self.1)) } } impl Canonicalize for BaseRepresentationFactor { type MergeKey = BaseEntry; fn merge_key(&self) -> Self::MergeKey { self.0.clone() // TODO(minor): can cloning be prevented here? } fn merge(self, other: Self) -> Self { BaseRepresentationFactor(self.0, self.1 + other.1) } fn is_trivial(&self) -> bool { self.1 == Rational::zero() } } impl Power for BaseRepresentationFactor { fn power(self, e: Exponent) -> Self { let BaseRepresentationFactor(entry, exp) = self; BaseRepresentationFactor(entry, exp * e) } } // TODO(minor): this could be represented with a base index in the first tuple component instead of a cloned string pub type BaseRepresentation = Product; impl Display for BaseRepresentation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.iter().count() == 0 { f.write_str("Scalar") } else { f.write_str(&self.as_string(|f| f.1, '×', '/', true)) } } } impl PrettyPrint for BaseRepresentation { fn pretty_print(&self) -> crate::markup::Markup { if self.iter().count() == 0 { crate::markup::type_identifier("Scalar") } else { self.pretty_print_with(|f| f.1, '×', '/', true, None) } } } #[derive(Debug, Clone)] pub struct Registry { base_entries: Vec<(String, Metadata)>, derived_entries: HashMap, } impl Default for Registry { fn default() -> Self { Self { base_entries: vec![], derived_entries: HashMap::default(), } } } impl Registry { pub fn add_base_entry(&mut self, name: &str, metadata: Metadata) -> Result<()> { if self.contains(name) { return Err(RegistryError::EntryExists(name.to_owned())); } self.base_entries.push((name.to_owned(), metadata)); Ok(()) } pub fn get_derived_entry_names_for( &self, base_representation: &BaseRepresentation, ) -> Vec { self.derived_entries .iter() .filter(|(_, (br, _))| br == base_representation) .map(|(name, _)| name.clone()) .sorted_unstable() .collect() } pub fn add_derived_entry( &mut self, name: &str, base_representation: BaseRepresentation, metadata: Metadata, ) -> Result<()> { if self.contains(name) { return Err(RegistryError::EntryExists(name.to_owned())); } self.derived_entries .insert(name.to_owned(), (base_representation, metadata)); Ok(()) } pub fn contains(&self, name: &str) -> bool { self.base_entries.iter().any(|(n, _)| n == name) || self.derived_entries.contains_key(name) } pub fn get_base_representation_for_name( &self, name: &str, ) -> Result<(BaseRepresentation, Metadata)> { if let Some(metadata) = self .base_entries .iter() .find(|(n, _)| n == name) .map(|(_, m)| m) { Ok(( BaseRepresentation::from_factor(BaseRepresentationFactor( name.to_owned(), Rational::from_integer(1), )), metadata.clone(), )) } else { self.derived_entries .get(name) .ok_or_else(|| { let suggestion = suggestion::did_you_mean( self.base_entries .iter() .map(|(id, _)| id.to_string()) .chain(self.derived_entries.keys().map(|s| s.to_string())), name, ); RegistryError::UnknownEntry(name.to_owned(), suggestion) }) .cloned() } } pub fn iter_base_entries(&self) -> impl Iterator + '_ { self.base_entries.iter().map(|(name, _)| name.clone()) } pub fn iter_derived_entries(&self) -> impl Iterator + '_ { self.derived_entries.keys().cloned() } } numbat-1.11.0/src/resolver.rs000064400000000000000000000215151046102023000141740ustar 00000000000000use std::{path::PathBuf, sync::Arc}; use crate::{ ast::Statement, module_importer::ModuleImporter, parser::parse, span::Span, ParseError, }; use codespan_reporting::files::SimpleFiles; use thiserror::Error; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct ModulePath(pub Vec); impl std::fmt::Display for ModulePath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", itertools::join(self.0.iter().cloned(), "::")) } } #[derive(Debug, Clone)] pub enum CodeSource { /// User input from the command line or a REPL Text, /// For internal use, e.g. when executing special statements (like "use prelude") /// during startup. Internal, /// A file that has been read in File(PathBuf), /// A module that has been imported Module(ModulePath, Option), } #[derive(Error, Clone, Debug)] pub enum ResolverError { #[error("Unknown module '{1}'.")] UnknownModule(Span, ModulePath), #[error("{}", .0.iter().map(|e| e.to_string()).collect::>().join("\n"))] ParseErrors(Vec), } type Result = std::result::Result; #[derive(Clone)] pub struct Resolver { importer: Arc, pub files: SimpleFiles, text_code_source_count: usize, internal_code_source_count: usize, imported_modules: Vec, } impl Resolver { pub(crate) fn new(importer: impl ModuleImporter + Send + 'static) -> Self { Self { importer: Arc::new(importer), files: SimpleFiles::new(), text_code_source_count: 0, internal_code_source_count: 0, imported_modules: vec![], } } fn add_code_source(&mut self, code_source: CodeSource, content: &str) -> usize { let code_source_name = match &code_source { CodeSource::Text => { self.text_code_source_count += 1; format!("", self.text_code_source_count) } CodeSource::Internal => { self.internal_code_source_count += 1; format!("", self.internal_code_source_count) } CodeSource::File(path) => format!("File {}", path.to_string_lossy()), CodeSource::Module(module_path, path) => format!( "Module '{module_path}', File {path}", module_path = itertools::join(module_path.0.iter(), "::"), path = path .as_ref() .map(|p| p.to_string_lossy().to_string()) .unwrap_or("?".into()), ), }; self.files.add(code_source_name, content.to_string()) } fn parse(&self, code: &str, code_source_id: usize) -> Result> { parse(code, code_source_id).map_err(|e| ResolverError::ParseErrors(e.1)) } fn inlining_pass(&mut self, program: &[Statement]) -> Result> { let mut new_program = vec![]; for statement in program { match statement { Statement::ModuleImport(span, module_path) => { if !self.imported_modules.contains(module_path) { if let Some((code, filesystem_path)) = self.importer.import(module_path) { self.imported_modules.push(module_path.clone()); let code_source_id = self.add_code_source( CodeSource::Module(module_path.clone(), filesystem_path), &code, ); let imported_program = self.parse(&code, code_source_id)?; let inlined_program = self.inlining_pass(&imported_program)?; for statement in inlined_program { new_program.push(statement); } } else { return Err(ResolverError::UnknownModule(*span, module_path.clone())); } } } statement => new_program.push(statement.clone()), } } Ok(new_program) } pub fn resolve(&mut self, code: &str, code_source: CodeSource) -> Result> { let code_source_id = self.add_code_source(code_source, code); let statements = self.parse(code, code_source_id)?; self.inlining_pass(&statements) } pub fn get_importer(&self) -> &dyn ModuleImporter { self.importer.as_ref() } } #[cfg(test)] mod tests { use crate::{ ast::{Expression, Statement}, number::Number, }; use super::*; struct TestImporter {} impl ModuleImporter for TestImporter { fn import(&self, path: &ModulePath) -> Option<(String, Option)> { match path { ModulePath(p) if p == &["foo", "bar"] => Some(("use foo::baz".into(), None)), ModulePath(p) if p == &["foo", "baz"] => Some(("let a = 1".into(), None)), // ---- ModulePath(p) if p == &["mod_a"] => Some(("use mod_b".into(), None)), ModulePath(p) if p == &["mod_b"] => Some(("use mod_c\n let x = y".into(), None)), ModulePath(p) if p == &["mod_c"] => Some(("let y = 1".into(), None)), // ---- ModulePath(p) if p == &["cycle_a"] => Some(("use cycle_b".into(), None)), ModulePath(p) if p == &["cycle_b"] => Some(("use cycle_a".into(), None)), _ => None, } } fn list_modules(&self) -> Vec { unimplemented!() } } #[test] fn resolver_basic_import() { use crate::ast::ReplaceSpans; let program = " use foo::bar a "; let importer = TestImporter {}; let mut resolver = Resolver::new(importer); let program_inlined = resolver.resolve(program, CodeSource::Internal).unwrap(); assert_eq!( &program_inlined.replace_spans(), &[ Statement::DefineVariable { identifier_span: Span::dummy(), identifier: "a".into(), expr: Expression::Scalar(Span::dummy(), Number::from_f64(1.0)), type_annotation: None, decorators: Vec::new(), }, Statement::Expression(Expression::Identifier(Span::dummy(), "a".into())) ] ); } #[test] fn resolver_repeated_includes() { use crate::ast::ReplaceSpans; let program = " use foo::bar use foo::bar a "; let importer = TestImporter {}; let mut resolver = Resolver::new(importer); let program_inlined = resolver.resolve(program, CodeSource::Internal).unwrap(); assert_eq!( &program_inlined.replace_spans(), &[ Statement::DefineVariable { identifier_span: Span::dummy(), identifier: "a".into(), expr: Expression::Scalar(Span::dummy(), Number::from_f64(1.0)), type_annotation: None, decorators: Vec::new(), }, Statement::Expression(Expression::Identifier(Span::dummy(), "a".into())) ] ); } #[test] fn resolver_depth_first_includes() { use crate::ast::ReplaceSpans; let program = " use mod_a use mod_c "; let importer = TestImporter {}; let mut resolver = Resolver::new(importer); let program_inlined = resolver.resolve(program, CodeSource::Internal).unwrap(); assert_eq!( &program_inlined.replace_spans(), &[ Statement::DefineVariable { identifier_span: Span::dummy(), identifier: "y".into(), expr: Expression::Scalar(Span::dummy(), Number::from_f64(1.0)), type_annotation: None, decorators: Vec::new(), }, Statement::DefineVariable { identifier_span: Span::dummy(), identifier: "x".into(), expr: Expression::Identifier(Span::dummy(), "y".into()), type_annotation: None, decorators: Vec::new(), }, ] ); } #[test] fn resolved_cyclic_imports() { let program = " use cycle_a "; let importer = TestImporter {}; let mut resolver = Resolver::new(importer); let program_inlined = resolver.resolve(program, CodeSource::Internal).unwrap(); assert_eq!(&program_inlined, &[]); } } numbat-1.11.0/src/span.rs000064400000000000000000000027241046102023000132750ustar 00000000000000use codespan_reporting::diagnostic::{Label, LabelStyle}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SourceCodePositition { pub byte: u32, pub line: u32, pub position: u32, } impl SourceCodePositition { pub fn start() -> Self { Self { byte: 0, line: 1, position: 1, } } pub fn single_character_span(&self, code_source_id: usize) -> Span { Span { start: *self, end: *self, code_source_id, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Span { pub start: SourceCodePositition, pub end: SourceCodePositition, pub code_source_id: usize, } impl Span { pub fn extend(&self, other: &Span) -> Span { assert_eq!(self.code_source_id, other.code_source_id); Span { start: std::cmp::min(self.start, other.start), end: std::cmp::max(self.end, other.end), code_source_id: self.code_source_id, } } pub fn diagnostic_label(&self, style: LabelStyle) -> Label { Label::new( style, self.code_source_id, (self.start.byte as usize)..(self.end.byte as usize), ) } #[cfg(test)] pub fn dummy() -> Span { Self { start: SourceCodePositition::start(), end: SourceCodePositition::start(), code_source_id: 0, } } } numbat-1.11.0/src/suggestion.rs000064400000000000000000000011411046102023000145130ustar 00000000000000pub fn did_you_mean, T: AsRef>( entries: impl Iterator, user_input: T, ) -> Option { if user_input.as_ref().len() < 3 { return None; } entries .map(|ref id| { ( id.as_ref().to_string(), strsim::damerau_levenshtein( &id.as_ref().to_lowercase(), &user_input.as_ref().to_lowercase(), ), ) }) .min_by_key(|(_, dist)| *dist) .filter(|(id, dist)| id.len() >= 2 && *dist <= 3) .map(|(id, _)| id) } numbat-1.11.0/src/tokenizer.rs000064400000000000000000000767001046102023000143530ustar 00000000000000use crate::span::{SourceCodePositition, Span}; use std::collections::HashMap; use std::sync::OnceLock; use thiserror::Error; #[derive(Debug, Clone, Error, PartialEq, Eq)] pub enum TokenizerErrorKind { #[error("Unexpected character: '{character}'")] UnexpectedCharacter { character: char }, #[error("Unexpected character in negative exponent")] UnexpectedCharacterInNegativeExponent { character: Option }, #[error("Unexpected character in number literal: '{0}'")] UnexpectedCharacterInNumberLiteral(char), #[error("Unexpected character in identifier: '{0}'")] UnexpectedCharacterInIdentifier(char), #[error("Expected digit")] ExpectedDigit { character: Option }, #[error("Expected base-{base} digit")] ExpectedDigitInBase { base: usize, character: Option, }, #[error("Unterminated string")] UnterminatedString, #[error("Unterminated string interpolation")] UnterminatedStringInterpolation, #[error("Unexpected '{{' inside string interpolation")] UnexpectedCurlyInInterpolation, } #[derive(Debug, Error, PartialEq, Eq)] #[error("{kind}")] pub struct TokenizerError { pub kind: TokenizerErrorKind, pub span: Span, } type Result = std::result::Result; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TokenKind { // Brackets LeftParen, RightParen, LeftBracket, RightBracket, // Operators and special signs Plus, Minus, Multiply, Power, Divide, Per, Comma, Arrow, Equal, Colon, DoubleColon, PostfixApply, UnicodeExponent, At, Ellipsis, ExclamationMark, EqualEqual, NotEqual, LessThan, GreaterThan, LessOrEqual, GreaterOrEqual, LogicalAnd, LogicalOr, // Keywords Let, Fn, // 'fn' Dimension, Unit, Use, To, Bool, True, False, If, Then, Else, String, DateTime, CapitalFn, // 'Fn' Long, Short, Both, None, // Procedure calls ProcedurePrint, ProcedureAssert, ProcedureAssertEq, ProcedureType, // Variable-length tokens Number, IntegerWithBase(usize), Identifier, // A normal string without interpolation: `"hello world"` StringFixed, // A part of a string which *starts* an interpolation: `"foo = {` StringInterpolationStart, // A part of a string between two interpolations: `}, and bar = {` StringInterpolationMiddle, // A part of a string which ends an interpolation: `}."` StringInterpolationEnd, // Other Newline, Eof, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Token { pub kind: TokenKind, pub lexeme: String, // TODO(minor): could be a &'str view into the input pub span: Span, } fn is_exponent_char(c: char) -> bool { matches!(c, '¹' | '²' | '³' | '⁴' | '⁵' | '⁶' | '⁷' | '⁸' | '⁹') } fn is_numerical_fraction_char(c: char) -> bool { matches!( c, '¼' | '½' | '¾' | '⅐' | '⅑' | '⅒' | '⅓' | '⅔' | '⅕' | '⅖' | '⅗' | '⅘' | '⅙' | '⅚' | '⅛' | '⅜' | '⅝' | '⅞' ) } fn is_currency_char(c: char) -> bool { let c_u32 = c as u32; // See https://en.wikipedia.org/wiki/Currency_Symbols_(Unicode_block) (0x20A0..=0x20CF).contains(&c_u32) || c == '£' || c == '¥' || c == '$' || c == '฿' } fn is_other_allowed_identifier_char(c: char) -> bool { c == '%' } fn is_subscript_char(c: char) -> bool { let c_u32 = c as u32; // See https://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts#Superscripts_and_subscripts_block (0x2080..=0x209CF).contains(&c_u32) } fn is_identifier_start(c: char) -> bool { unicode_ident::is_xid_start(c) || is_numerical_fraction_char(c) || is_currency_char(c) || is_other_allowed_identifier_char(c) || c == '°' || c == '_' } fn is_identifier_continue(c: char) -> bool { (unicode_ident::is_xid_continue(c) || is_subscript_char(c) || is_currency_char(c) || is_other_allowed_identifier_char(c)) && !is_exponent_char(c) && c != '·' } /// When scanning a string interpolation like `"foo = {foo}, and bar = {bar}."`, /// the tokenizer needs to keep track of where it currently is, because we allow /// for (almost) arbitrary expressions inside the {…} part. enum InterpolationState { /// We are not inside curly braces. Outside, /// We are currently scanning the inner part of an interpolation. Inside, } impl InterpolationState { fn is_inside(&self) -> bool { matches!(self, InterpolationState::Inside) } } struct Tokenizer { input: Vec, current: SourceCodePositition, last: SourceCodePositition, token_start: SourceCodePositition, current_index: usize, token_start_index: usize, code_source_id: usize, // Special fields / state for parsing string interpolations string_start: SourceCodePositition, interpolation_start: SourceCodePositition, interpolation_state: InterpolationState, } impl Tokenizer { fn new(input: &str, code_source_id: usize) -> Self { Tokenizer { input: input.chars().collect(), current: SourceCodePositition::start(), last: SourceCodePositition::start(), token_start: SourceCodePositition::start(), current_index: 0, token_start_index: 0, code_source_id, string_start: SourceCodePositition::start(), interpolation_start: SourceCodePositition::start(), interpolation_state: InterpolationState::Outside, } } fn scan(&mut self) -> Result> { let mut tokens = vec![]; while !self.at_end() { self.token_start = self.current; self.token_start_index = self.current_index; if let Some(token) = self.scan_single_token()? { tokens.push(token); } } tokens.push(Token { kind: TokenKind::Eof, lexeme: "".into(), span: self.current.single_character_span(self.code_source_id), }); Ok(tokens) } fn consume_stream_of_digits( &mut self, at_least_one_digit: bool, disallow_leading_underscore: bool, disallow_dot_after_stream: bool, ) -> Result<()> { if at_least_one_digit && !self.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) { return Err(TokenizerError { kind: TokenizerErrorKind::ExpectedDigit { character: self.peek(), }, span: self.current.single_character_span(self.code_source_id), }); } // Make sure we don't start with an underscore if disallow_leading_underscore && self.peek().map(|c| c == '_').unwrap_or(false) { return Err(TokenizerError { kind: TokenizerErrorKind::UnexpectedCharacterInNumberLiteral(self.peek().unwrap()), span: self.current.single_character_span(self.code_source_id), }); } let mut last_char = None; while self .peek() .map(|c| c.is_ascii_digit() || c == '_') .unwrap_or(false) { last_char = Some(self.advance()); } // Make sure we don't end with an underscore if last_char == Some('_') { return Err(TokenizerError { kind: TokenizerErrorKind::UnexpectedCharacterInNumberLiteral('_'), span: self.last.single_character_span(self.code_source_id), }); } if disallow_dot_after_stream && self.peek().map(|c| c == '.').unwrap_or(false) { return Err(TokenizerError { kind: TokenizerErrorKind::UnexpectedCharacterInNumberLiteral(self.peek().unwrap()), span: self.current.single_character_span(self.code_source_id), }); } Ok(()) } fn scientific_notation(&mut self) -> Result<()> { if self .peek2() .map(|c| c.is_ascii_digit() || c == '+' || c == '-') .unwrap_or(false) && (self.match_char('e') || self.match_char('E')) { let _ = self.match_char('+') || self.match_char('-'); self.consume_stream_of_digits(true, true, true)?; } Ok(()) } fn scan_single_token(&mut self) -> Result> { static KEYWORDS: OnceLock> = OnceLock::new(); let keywords = KEYWORDS.get_or_init(|| { let mut m = HashMap::new(); m.insert("per", TokenKind::Per); m.insert("to", TokenKind::To); m.insert("let", TokenKind::Let); m.insert("fn", TokenKind::Fn); m.insert("dimension", TokenKind::Dimension); m.insert("unit", TokenKind::Unit); m.insert("use", TokenKind::Use); m.insert("long", TokenKind::Long); m.insert("short", TokenKind::Short); m.insert("both", TokenKind::Both); m.insert("none", TokenKind::None); m.insert("print", TokenKind::ProcedurePrint); m.insert("assert", TokenKind::ProcedureAssert); m.insert("assert_eq", TokenKind::ProcedureAssertEq); m.insert("type", TokenKind::ProcedureType); m.insert("Bool", TokenKind::Bool); m.insert("true", TokenKind::True); m.insert("false", TokenKind::False); m.insert("if", TokenKind::If); m.insert("then", TokenKind::Then); m.insert("else", TokenKind::Else); m.insert("String", TokenKind::String); m.insert("DateTime", TokenKind::DateTime); m.insert("Fn", TokenKind::CapitalFn); // Keep this list in sync with keywords::KEYWORDS! m }); if self.peek() == Some('#') { // skip over comment until newline loop { match self.peek() { None => return Ok(None), Some('\n') => break, _ => { self.advance(); } } } } let current_char = self.advance(); let code_source_id = self.code_source_id; let tokenizer_error = |position: &SourceCodePositition, kind| -> Result> { Err(TokenizerError { kind, span: position.single_character_span(code_source_id), }) }; let kind = match current_char { '(' => TokenKind::LeftParen, ')' => TokenKind::RightParen, '[' => TokenKind::LeftBracket, ']' => TokenKind::RightBracket, '≤' => TokenKind::LessOrEqual, '<' if self.match_char('=') => TokenKind::LessOrEqual, '<' => TokenKind::LessThan, '≥' => TokenKind::GreaterOrEqual, '>' if self.match_char('=') => TokenKind::GreaterOrEqual, '>' => TokenKind::GreaterThan, '0' if self .peek() .map(|c| c == 'x' || c == 'o' || c == 'b') .unwrap_or(false) => { let (base, is_digit_in_base): (_, Box bool>) = match self.peek().unwrap() { 'x' => (16, Box::new(|c| c.is_ascii_hexdigit())), 'o' => (8, Box::new(|c| ('0'..='7').contains(&c))), 'b' => (2, Box::new(|c| c == '0' || c == '1')), _ => unreachable!(), }; self.advance(); // skip over the x/o/b // If the first character is not a digits, that's an error. if !self.peek().map(&is_digit_in_base).unwrap_or(false) { return tokenizer_error( &self.current, TokenizerErrorKind::ExpectedDigitInBase { base, character: self.peek(), }, ); } let mut last_char = None; while self .peek() .map(|c| is_digit_in_base(c) || c == '_') .unwrap_or(false) { last_char = self.peek(); self.advance(); } // Numeric literal should not end with a `_` either. if last_char == Some('_') || self .peek() .map(|c| is_identifier_continue(c) || c == '.') .unwrap_or(false) { return tokenizer_error( &self.current, TokenizerErrorKind::ExpectedDigitInBase { base, character: self.peek(), }, ); } TokenKind::IntegerWithBase(base) } c if c.is_ascii_digit() => { self.consume_stream_of_digits(false, false, false)?; // decimal part if self.match_char('.') { self.consume_stream_of_digits(false, true, true)?; } self.scientific_notation()?; TokenKind::Number } '.' => { self.consume_stream_of_digits(true, true, true)?; self.scientific_notation()?; TokenKind::Number } ' ' | '\t' | '\r' => { return Ok(None); } '\n' => TokenKind::Newline, '&' if self.match_char('&') => TokenKind::LogicalAnd, '|' if self.match_char('|') => TokenKind::LogicalOr, '*' if self.match_char('*') => TokenKind::Power, '+' => TokenKind::Plus, '*' | '·' | '⋅' | '×' => TokenKind::Multiply, '/' if self.match_char('/') => TokenKind::PostfixApply, '/' => TokenKind::Divide, '÷' => TokenKind::Divide, '^' => TokenKind::Power, ',' => TokenKind::Comma, '⩵' => TokenKind::EqualEqual, '=' if self.match_char('=') => TokenKind::EqualEqual, '=' => TokenKind::Equal, ':' if self.match_char(':') => TokenKind::DoubleColon, ':' => TokenKind::Colon, '@' => TokenKind::At, '→' | '➞' => TokenKind::Arrow, '-' if self.match_char('>') => TokenKind::Arrow, '-' | '−' => TokenKind::Minus, '≠' => TokenKind::NotEqual, '!' if self.match_char('=') => TokenKind::NotEqual, '!' => TokenKind::ExclamationMark, '⁻' => { let c = self.peek(); if c.map(is_exponent_char).unwrap_or(false) { self.advance(); TokenKind::UnicodeExponent } else { return tokenizer_error( &self.current, TokenizerErrorKind::UnexpectedCharacterInNegativeExponent { character: c }, ); } } '¹' | '²' | '³' | '⁴' | '⁵' | '⁶' | '⁷' | '⁸' | '⁹' => { TokenKind::UnicodeExponent } '"' => match self.interpolation_state { InterpolationState::Outside => { self.string_start = self.token_start; while self.peek().map(|c| c != '"' && c != '{').unwrap_or(false) { self.advance(); } if self.match_char('"') { TokenKind::StringFixed } else if self.match_char('{') { self.interpolation_state = InterpolationState::Inside; self.interpolation_start = self.last; TokenKind::StringInterpolationStart } else { return Err(TokenizerError { kind: TokenizerErrorKind::UnterminatedString, span: Span { start: self.token_start, end: self.current, code_source_id: self.code_source_id, }, }); } } InterpolationState::Inside => { return Err(TokenizerError { kind: TokenizerErrorKind::UnterminatedStringInterpolation, span: Span { start: self.interpolation_start, end: self.last, code_source_id: self.code_source_id, }, }); } }, '}' if self.interpolation_state.is_inside() => { while self.peek().map(|c| c != '"' && c != '{').unwrap_or(false) { self.advance(); } if self.match_char('"') { self.interpolation_state = InterpolationState::Outside; TokenKind::StringInterpolationEnd } else if self.match_char('{') { self.interpolation_start = self.last; TokenKind::StringInterpolationMiddle } else { return Err(TokenizerError { kind: TokenizerErrorKind::UnterminatedString, span: Span { start: self.string_start, end: self.current, code_source_id: self.code_source_id, }, }); } } '{' if self.interpolation_state.is_inside() => { return Err(TokenizerError { kind: TokenizerErrorKind::UnexpectedCurlyInInterpolation, span: self.last.single_character_span(code_source_id), }); } '…' => TokenKind::Ellipsis, c if is_identifier_start(c) => { while self.peek().map(is_identifier_continue).unwrap_or(false) { self.advance(); } if self.peek().map(|c| c == '.').unwrap_or(false) { return tokenizer_error( &self.current, TokenizerErrorKind::UnexpectedCharacterInIdentifier(self.peek().unwrap()), ); } if let Some(kind) = keywords.get(self.lexeme().as_str()) { *kind } else { TokenKind::Identifier } } c => { return tokenizer_error( &self.token_start, TokenizerErrorKind::UnexpectedCharacter { character: c }, ); } }; let token = Some(Token { kind, lexeme: self.lexeme(), span: Span { start: self.token_start, end: self.current, code_source_id: self.code_source_id, }, }); if kind == TokenKind::Newline { self.current.line += 1; self.current.position = 1; } Ok(token) } fn lexeme(&self) -> String { self.input[self.token_start_index..self.current_index] .iter() .collect() } fn advance(&mut self) -> char { let c = self.input[self.current_index]; self.last = self.current; self.current_index += 1; self.current.byte += c.len_utf8() as u32; self.current.position += 1; c } fn peek(&self) -> Option { self.input.get(self.current_index).copied() } fn peek2(&self) -> Option { self.input.get(self.current_index + 1).copied() } fn match_char(&mut self, c: char) -> bool { if self.peek() == Some(c) { self.advance(); true } else { false } } fn at_end(&self) -> bool { self.current_index >= self.input.len() } } pub fn tokenize(input: &str, code_source_id: usize) -> Result> { let mut tokenizer = Tokenizer::new(input, code_source_id); tokenizer.scan() } #[cfg(test)] fn tokenize_reduced(input: &str) -> Result, String> { Ok(tokenize(input, 0) .map_err(|e| { format!( "Error at {:?}: `{e}`", (e.span.start.line, e.span.start.position) ) })? .iter() .map(|token| { ( token.lexeme.to_string(), token.kind, (token.span.start.line, token.span.start.position), ) }) .collect()) } #[cfg(test)] fn tokenize_reduced_pretty(input: &str) -> Result { use std::fmt::Write; let mut ret = String::new(); for (lexeme, kind, pos) in tokenize_reduced(input)? { writeln!(ret, "{lexeme:?}, {kind:?}, {pos:?}").unwrap(); } Ok(ret) } #[test] fn test_tokenize_basic() { use TokenKind::*; assert_eq!( tokenize_reduced(" 12 + 34 ").unwrap(), [ ("12".to_string(), Number, (1, 3)), ("+".to_string(), Plus, (1, 6)), ("34".to_string(), Number, (1, 8)), ("".to_string(), Eof, (1, 12)) ] ); assert_eq!( tokenize_reduced("1 2").unwrap(), [ ("1".to_string(), Number, (1, 1)), ("2".to_string(), Number, (1, 3)), ("".to_string(), Eof, (1, 4)) ] ); assert_eq!( tokenize_reduced("12 × (3 - 4)").unwrap(), [ ("12".to_string(), Number, (1, 1)), ("×".to_string(), Multiply, (1, 4)), ("(".to_string(), LeftParen, (1, 6)), ("3".to_string(), Number, (1, 7)), ("-".to_string(), Minus, (1, 9)), ("4".to_string(), Number, (1, 11)), (")".to_string(), RightParen, (1, 12)), ("".to_string(), Eof, (1, 13)) ] ); assert_eq!( tokenize_reduced("foo to bar").unwrap(), [ ("foo".to_string(), Identifier, (1, 1)), ("to".to_string(), To, (1, 5)), ("bar".to_string(), Identifier, (1, 8)), ("".to_string(), Eof, (1, 11)) ] ); assert_eq!( tokenize_reduced("1 -> 2").unwrap(), [ ("1".to_string(), Number, (1, 1)), ("->".to_string(), Arrow, (1, 3)), ("2".to_string(), Number, (1, 6)), ("".to_string(), Eof, (1, 7)) ] ); assert_eq!( tokenize_reduced("45°").unwrap(), [ ("45".to_string(), Number, (1, 1)), ("°".to_string(), Identifier, (1, 3)), ("".to_string(), Eof, (1, 4)) ] ); assert_eq!( tokenize_reduced("1+2\n42").unwrap(), [ ("1".to_string(), Number, (1, 1)), ("+".to_string(), Plus, (1, 2)), ("2".to_string(), Number, (1, 3)), ("\n".to_string(), Newline, (1, 4)), ("42".to_string(), Number, (2, 1)), ("".to_string(), Eof, (2, 3)) ] ); insta::assert_snapshot!( tokenize_reduced_pretty("~").unwrap_err(), @"Error at (1, 1): `Unexpected character: '~'`"); } #[test] fn test_tokenize_numbers() { // valid queries insta::assert_snapshot!( tokenize_reduced_pretty("12").unwrap(), @r###" "12", Number, (1, 1) "", Eof, (1, 3) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("1_2").unwrap(), @r###" "1_2", Number, (1, 1) "", Eof, (1, 4) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("1.").unwrap(), @r###" "1.", Number, (1, 1) "", Eof, (1, 3) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("1.2").unwrap(), @r###" "1.2", Number, (1, 1) "", Eof, (1, 4) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("1_1.2_2").unwrap(), @r###" "1_1.2_2", Number, (1, 1) "", Eof, (1, 8) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("0b01").unwrap(), @r###" "0b01", IntegerWithBase(2), (1, 1) "", Eof, (1, 5) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("0b1_0").unwrap(), @r###" "0b1_0", IntegerWithBase(2), (1, 1) "", Eof, (1, 6) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("0o01234567").unwrap(), @r###" "0o01234567", IntegerWithBase(8), (1, 1) "", Eof, (1, 11) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("0o1_2").unwrap(), @r###" "0o1_2", IntegerWithBase(8), (1, 1) "", Eof, (1, 6) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("0x1234567890abcdef").unwrap(), @r###" "0x1234567890abcdef", IntegerWithBase(16), (1, 1) "", Eof, (1, 19) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("0x1_2").unwrap(), @r###" "0x1_2", IntegerWithBase(16), (1, 1) "", Eof, (1, 6) "### ); // Failing queries insta::assert_snapshot!( tokenize_reduced_pretty("1_.2").unwrap_err(), @"Error at (1, 2): `Unexpected character in number literal: '_'`" ); insta::assert_snapshot!( tokenize_reduced_pretty("1.2_").unwrap_err(), @"Error at (1, 4): `Unexpected character in number literal: '_'`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0b012").unwrap_err(), @"Error at (1, 5): `Expected base-2 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0b").unwrap_err(), @"Error at (1, 3): `Expected base-2 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0b_").unwrap_err(), @"Error at (1, 3): `Expected base-2 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0b_1").unwrap_err(), @"Error at (1, 3): `Expected base-2 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0b1_").unwrap_err(), @"Error at (1, 5): `Expected base-2 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0o012345678").unwrap_err(), @"Error at (1, 11): `Expected base-8 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0o").unwrap_err(), @"Error at (1, 3): `Expected base-8 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0o_").unwrap_err(), @"Error at (1, 3): `Expected base-8 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0o_1").unwrap_err(), @"Error at (1, 3): `Expected base-8 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0o1_").unwrap_err(), @"Error at (1, 5): `Expected base-8 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0x1234567890abcdefg").unwrap_err(), @"Error at (1, 19): `Expected base-16 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0x").unwrap_err(), @"Error at (1, 3): `Expected base-16 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0x_").unwrap_err(), @"Error at (1, 3): `Expected base-16 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0x_1").unwrap_err(), @"Error at (1, 3): `Expected base-16 digit`" ); insta::assert_snapshot!( tokenize_reduced_pretty("0x1_").unwrap_err(), @"Error at (1, 5): `Expected base-16 digit`" ); } #[test] fn test_tokenize_string() { use TokenKind::*; assert_eq!( tokenize_reduced("\"foo\"").unwrap(), [ ("\"foo\"".to_string(), StringFixed, (1, 1)), ("".to_string(), Eof, (1, 6)) ] ); assert_eq!( tokenize_reduced("\"foo = {foo}\"").unwrap(), [ ("\"foo = {".to_string(), StringInterpolationStart, (1, 1)), ("foo".to_string(), Identifier, (1, 9)), ("}\"".to_string(), StringInterpolationEnd, (1, 12)), ("".to_string(), Eof, (1, 14)) ] ); assert_eq!( tokenize_reduced("\"foo = {foo}, and bar = {bar}\"").unwrap(), [ ("\"foo = {".to_string(), StringInterpolationStart, (1, 1)), ("foo".to_string(), Identifier, (1, 9)), ( "}, and bar = {".to_string(), StringInterpolationMiddle, (1, 12) ), ("bar".to_string(), Identifier, (1, 26)), ("}\"".to_string(), StringInterpolationEnd, (1, 29)), ("".to_string(), Eof, (1, 31)) ] ); assert_eq!( tokenize_reduced("\"1 + 2 = {1 + 2}\"").unwrap(), [ ("\"1 + 2 = {".to_string(), StringInterpolationStart, (1, 1)), ("1".to_string(), Number, (1, 11)), ("+".to_string(), Plus, (1, 13)), ("2".to_string(), Number, (1, 15)), ("}\"".to_string(), StringInterpolationEnd, (1, 16)), ("".to_string(), Eof, (1, 18)) ] ); assert_eq!( tokenize("\"foo", 0).unwrap_err().kind, TokenizerErrorKind::UnterminatedString ); assert_eq!( tokenize("\"foo = {foo\"", 0).unwrap_err().kind, TokenizerErrorKind::UnterminatedStringInterpolation ); assert_eq!( tokenize("\"foo = {foo}.", 0).unwrap_err().kind, TokenizerErrorKind::UnterminatedString ); assert_eq!( tokenize("\"foo = {foo, bar = {bar}\"", 0).unwrap_err().kind, TokenizerErrorKind::UnexpectedCurlyInInterpolation ); } #[test] fn test_logical_operators() { insta::assert_snapshot!( tokenize_reduced_pretty("true || false").unwrap(), @r###" "true", True, (1, 1) "||", LogicalOr, (1, 6) "false", False, (1, 9) "", Eof, (1, 14) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("true && false").unwrap(), @r###" "true", True, (1, 1) "&&", LogicalAnd, (1, 6) "false", False, (1, 9) "", Eof, (1, 14) "### ); insta::assert_snapshot!( tokenize_reduced_pretty("true | false").unwrap_err(), @"Error at (1, 6): `Unexpected character: '|'`" ); insta::assert_snapshot!( tokenize_reduced_pretty("true & false").unwrap_err(), @"Error at (1, 6): `Unexpected character: '&'`" ); } #[test] fn test_is_currency_char() { assert!(is_currency_char('€')); assert!(is_currency_char('$')); assert!(is_currency_char('¥')); assert!(is_currency_char('£')); assert!(is_currency_char('฿')); assert!(is_currency_char('₿')); assert!(!is_currency_char('E')); } #[test] fn test_is_subscript_char() { assert!(is_subscript_char('₅')); assert!(is_subscript_char('₁')); assert!(is_subscript_char('ₓ')); assert!(is_subscript_char('ₘ')); assert!(is_subscript_char('₎')); } numbat-1.11.0/src/typechecker.rs000064400000000000000000002635101046102023000146440ustar 00000000000000use std::{ collections::{HashMap, HashSet}, error::Error, fmt, }; use crate::typed_ast::{self, Type}; use crate::{ arithmetic::{pretty_exponent, Exponent, Power, Rational}, ast::ProcedureKind, }; use crate::{ast, decorator, ffi, suggestion}; use crate::{ast::StringPart, span::Span}; use crate::{ ast::TypeAnnotation, registry::{BaseRepresentation, BaseRepresentationFactor, RegistryError}, }; use crate::{dimension::DimensionRegistry, typed_ast::DType}; use crate::{ffi::ArityRange, typed_ast::Expression}; use crate::{name_resolution::LAST_RESULT_IDENTIFIERS, pretty_print::PrettyPrint}; use ast::{BinaryOperator, DimensionExpression}; use itertools::Itertools; use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, FromPrimitive, Zero}; use thiserror::Error; use unicode_width::UnicodeWidthStr; #[derive(Debug, Clone, PartialEq, Eq)] pub struct IncompatibleDimensionsError { pub span_operation: Span, pub operation: String, pub span_expected: Span, pub expected_name: &'static str, pub expected_type: BaseRepresentation, pub expected_dimensions: Vec, pub span_actual: Span, pub actual_name: &'static str, pub actual_name_for_fix: &'static str, pub actual_type: BaseRepresentation, pub actual_dimensions: Vec, } fn pad(a: &str, b: &str) -> (String, String) { let max_length = a.width().max(b.width()); ( format!("{a: Option { // Heuristic 1: if actual_type == 1 / expected_type, suggest // to invert the 'actual' expression: if actual_type == &expected_type.clone().invert() { return Some(format!("invert the {expression_to_change}")); } // Heuristic 2: compute the "missing" factor between the expected // and the actual type. Suggest to multiply / divide with the // appropriate delta. let delta_type = expected_type.clone() / actual_type.clone(); let num_factors = delta_type.iter().count(); if num_factors > 1 { return None; // Do not suggest fixes with complicated dimensions } let exponent_sum: Rational = delta_type.iter().map(|a| a.1).sum(); let (action, delta_type) = if exponent_sum >= Rational::zero() { ("multiply", delta_type) } else { ("divide", delta_type.invert()) }; Some(format!( "{action} the {expression_to_change} by a `{delta_type}` factor" )) } impl fmt::Display for IncompatibleDimensionsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let have_common_factors = self .expected_type .iter() .any(|f| self.actual_type.iter().map(|f| &f.0).contains(&f.0)); let (mut expected_result_string, mut actual_result_string) = if !have_common_factors || (self.expected_type.iter().count() == 1 && self.actual_type.iter().count() == 1) { pad( &self.expected_type.to_string(), &self.actual_type.to_string(), ) } else { let format_factor = |name: &str, exponent: &Exponent| format!(" × {name}{}", pretty_exponent(exponent)); let mut shared_factors = HashMap::<&String, (Exponent, Exponent)>::new(); let mut expected_factors = HashMap::<&String, Exponent>::new(); let mut actual_factors = HashMap::<&String, Exponent>::new(); for BaseRepresentationFactor(name, expected_exponent) in self.expected_type.iter() { if let Some(BaseRepresentationFactor(_, actual_exponent)) = self.actual_type.iter().find(|f| *name == f.0) { shared_factors.insert(name, (*expected_exponent, *actual_exponent)); } else { expected_factors.insert(name, *expected_exponent); } } for BaseRepresentationFactor(name, exponent) in self.actual_type.iter() { if !shared_factors.contains_key(&name) { actual_factors.insert(name, *exponent); } } let mut expected_result_string = String::new(); let mut actual_result_string = String::new(); for (name, (exp1, exp2)) in shared_factors .iter() .sorted_unstable_by_key(|entry| entry.0) { let (str1, str2) = pad(&format_factor(name, exp1), &format_factor(name, exp2)); expected_result_string.push_str(&str1); actual_result_string.push_str(&str2); } let mut expected_factors_string = String::new(); for (name, exp) in expected_factors .iter() .sorted_unstable_by_key(|entry| entry.0) { expected_factors_string.push_str(&format_factor(name, exp)); } let mut actual_factors_string = String::new(); for (name, exp) in actual_factors .iter() .sorted_unstable_by_key(|entry| entry.0) { actual_factors_string.push_str(&format_factor(name, exp)); } expected_result_string.push_str(&format!( "{expected_factors_string: ), #[error(transparent)] IncompatibleDimensions(IncompatibleDimensionsError), #[error("Exponents need to be dimensionless (got {1}).")] NonScalarExponent(Span, DType), #[error("Argument of factorial needs to be dimensionless (got {1}).")] NonScalarFactorialArgument(Span, DType), #[error("Unsupported expression in const-evaluation of exponent: {1}.")] UnsupportedConstEvalExpression(Span, &'static str), #[error("Division by zero in const. eval. expression")] DivisionByZeroInConstEvalExpression(Span), #[error("{0}")] RegistryError(RegistryError), #[error("Incompatible alternative expressions have been provided for dimension '{0}'")] IncompatibleAlternativeDimensionExpression(String, Span, DType, Span, DType), #[error("Function or procedure '{callable_name}' called with {num_args} arguments(s), but needs {}..{}", arity.start(), arity.end())] WrongArity { callable_span: Span, callable_name: String, callable_definition_span: Option, arity: ArityRange, num_args: usize, }, #[error("'{1}' can not be used as a type parameter because it is also an existing dimension identifier.")] TypeParameterNameClash(Span, String), #[error("Could not infer the type parameters {3} in the function call '{2}'.")] CanNotInferTypeParameters(Span, Span, String, String), #[error("Multiple unresolved generic parameters in a single function parameter type are not (yet) supported. Consider reordering the function parameters")] MultipleUnresolvedTypeParameters(Span, Span), #[error("Foreign function definition (without body) '{1}' needs parameter and return type annotations.")] ForeignFunctionNeedsTypeAnnotations(Span, String), #[error("Unknown foreign function (without body) '{1}'")] UnknownForeignFunction(Span, String), #[error("Out-of bounds or non-rational exponent value")] NonRationalExponent(Span), #[error("Numerical overflow in const-eval expression")] OverflowInConstExpr(Span), #[error("Expected dimension type, got {1} instead")] ExpectedDimensionType(Span, Type), #[error("Expected boolean value")] ExpectedBool(Span), #[error("Incompatible types in condition")] IncompatibleTypesInCondition(Span, Type, Span, Type, Span), #[error("Argument types in assert call must be boolean")] IncompatibleTypeInAssert(Span, Type, Span), #[error("Argument types in assert_eq calls must match")] IncompatibleTypesInAssertEq(Span, Type, Span, Type, Span), #[error("Incompatible types in {0}")] IncompatibleTypesInAnnotation(String, Span, Type, Span, Type, Span), #[error("Incompatible types in comparison operator")] IncompatibleTypesInComparison(Span, Type, Span, Type, Span), #[error("Incompatible types in operator")] IncompatibleTypesInOperator(Span, BinaryOperator, Type, Span, Type, Span), #[error("Incompatible types in function call: expected '{1}', got '{3}' instead")] IncompatibleTypesInFunctionCall(Option, Type, Span, Type), #[error("This name is already used by {0}")] NameAlreadyUsedBy(&'static str, Span, Option), #[error("Missing a definition for dimension {1}")] MissingDimension(Span, String), #[error("Function references can not point to generic functions")] NoFunctionReferenceToGenericFunction(Span), #[error("Only functions and function references can be called")] OnlyFunctionsAndReferencesCanBeCalled(Span), #[error("Base units can not be dimensionless.")] NoDimensionlessBaseUnit(Span, String), } type Result = std::result::Result; fn to_rational_exponent(exponent_f64: f64) -> Option { Rational::from_f64(exponent_f64) } fn dtype(e: &Expression) -> Result { match e.get_type() { Type::Dimension(dtype) => Ok(dtype), t => Err(TypeCheckError::ExpectedDimensionType(e.full_span(), t)), } } /// Evaluates a limited set of expressions *at compile time*. This is needed to /// support type checking of expressions like `(2 * meter)^(2*3 - 4)` where we /// need to know not just the *type* but also the *value* of the exponent. fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result { match expr { typed_ast::Expression::Scalar(span, n) => { Ok(to_rational_exponent(n.to_f64()) .ok_or(TypeCheckError::NonRationalExponent(*span))?) } typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::Negate, ref expr, _) => { Ok(-evaluate_const_expr(expr)?) } e @ typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::Factorial, _, _) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "factorial"), ), e @ typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::LogicalNeg, _, _) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "logical"), ), e @ typed_ast::Expression::BinaryOperator(_span_op, op, lhs_expr, rhs_expr, _) => { let lhs = evaluate_const_expr(lhs_expr)?; let rhs = evaluate_const_expr(rhs_expr)?; match op { typed_ast::BinaryOperator::Add => Ok(lhs .checked_add(&rhs) .ok_or_else(|| TypeCheckError::OverflowInConstExpr(expr.full_span()))?), typed_ast::BinaryOperator::Sub => Ok(lhs .checked_sub(&rhs) .ok_or_else(|| TypeCheckError::OverflowInConstExpr(expr.full_span()))?), typed_ast::BinaryOperator::Mul => Ok(lhs .checked_mul(&rhs) .ok_or_else(|| TypeCheckError::OverflowInConstExpr(expr.full_span()))?), typed_ast::BinaryOperator::Div => { if rhs == Rational::zero() { Err(TypeCheckError::DivisionByZeroInConstEvalExpression( e.full_span(), )) } else { Ok(lhs .checked_div(&rhs) .ok_or_else(|| TypeCheckError::OverflowInConstExpr(expr.full_span()))?) } } typed_ast::BinaryOperator::Power => { if rhs.is_integer() { Ok(num_traits::checked_pow( lhs, rhs.to_integer().try_into().map_err(|_| { TypeCheckError::OverflowInConstExpr(expr.full_span()) })?, ) .ok_or_else(|| TypeCheckError::OverflowInConstExpr(expr.full_span()))?) } else { Err(TypeCheckError::UnsupportedConstEvalExpression( e.full_span(), "exponentiation with non-integer exponent", )) } } typed_ast::BinaryOperator::ConvertTo => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "conversion"), ), typed_ast::BinaryOperator::LessThan | typed_ast::BinaryOperator::GreaterThan | typed_ast::BinaryOperator::LessOrEqual | typed_ast::BinaryOperator::GreaterOrEqual | typed_ast::BinaryOperator::Equal | typed_ast::BinaryOperator::NotEqual => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "comparison"), ), typed_ast::BinaryOperator::LogicalAnd | typed_ast::BinaryOperator::LogicalOr => { Err(TypeCheckError::UnsupportedConstEvalExpression( e.full_span(), "logical", )) } } } e @ typed_ast::Expression::Identifier(..) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "variable"), ), e @ typed_ast::Expression::UnitIdentifier(..) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "unit identifier"), ), e @ typed_ast::Expression::FunctionCall(_, _, _, _, _) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "function call"), ), e @ &typed_ast::Expression::CallableCall(_, _, _, _) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "function call"), ), e @ typed_ast::Expression::Boolean(_, _) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "Boolean value"), ), e @ typed_ast::Expression::String(_, _) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "String"), ), e @ typed_ast::Expression::Condition(..) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "Conditional"), ), e @ Expression::BinaryOperatorForDate(..) => { Err(TypeCheckError::UnsupportedConstEvalExpression( e.full_span(), "binary operator for datetimes", )) } } } #[derive(Clone)] struct FunctionSignature { definition_span: Span, type_parameters: Vec<(Span, String)>, parameter_types: Vec<(Span, Type)>, is_variadic: bool, return_type: Type, is_foreign: bool, } #[derive(Clone, Default)] pub struct TypeChecker { identifiers: HashMap)>, function_signatures: HashMap, registry: DimensionRegistry, } impl TypeChecker { fn identifier_type(&self, span: Span, name: &str) -> Result { let id = self .identifiers .get(name) .ok_or_else(|| { let suggestion = suggestion::did_you_mean( self.identifiers .keys() .map(|k| k.to_string()) .chain(["true".into(), "false".into()]) // These are parsed as keywords, but can act like identifiers .chain(self.function_signatures.keys().cloned()) .chain(ffi::procedures().values().map(|p| p.name.clone())), name, ); TypeCheckError::UnknownIdentifier(span, name.into(), suggestion) }) .map(|(type_, _)| type_) .cloned(); if id.is_err() { if let Some(signature) = self.function_signatures.get(name) { if !signature.type_parameters.is_empty() { return Err(TypeCheckError::NoFunctionReferenceToGenericFunction(span)); } Ok(Type::Fn( signature .parameter_types .iter() .map(|(_, t)| t.clone()) .collect(), Box::new(signature.return_type.clone()), )) } else { id } } else { id } } fn get_proper_function_reference( &self, expr: &ast::Expression, ) -> Option<(String, &FunctionSignature)> { match expr { ast::Expression::Identifier(_, name) => self .function_signatures .get(name) .map(|signature| (name.clone(), signature)), _ => None, } } fn proper_function_call( &self, span: &Span, full_span: &Span, function_name: &str, signature: &FunctionSignature, arguments: Vec, argument_types: Vec, ) -> Result { let FunctionSignature { definition_span, type_parameters, parameter_types, is_variadic, return_type, is_foreign: _, } = signature; let arity_range = if *is_variadic { 1..=usize::MAX } else { parameter_types.len()..=parameter_types.len() }; if !arity_range.contains(&arguments.len()) { return Err(TypeCheckError::WrongArity { callable_span: *span, callable_name: function_name.into(), callable_definition_span: Some(*definition_span), arity: arity_range, num_args: arguments.len(), }); } let mut substitutions: Vec<(String, DType)> = vec![]; let substitute = |substitutions: &[(String, DType)], type_: &DType| -> DType { let mut result_type = type_.clone(); for (name, substituted_type) in substitutions { if let Some(factor @ BaseRepresentationFactor(_, exp)) = type_ .clone() // TODO: remove this .clone() somehow? .iter() .find(|BaseRepresentationFactor(n, _)| n == name) { result_type = result_type / DType::from_factor((*factor).clone()) * substituted_type.clone().power(*exp); } } result_type }; let mut parameter_types = parameter_types.clone(); if *is_variadic { // For a variadic function, we simply duplicate the parameter type // N times, where N is the number of arguments given. debug_assert!(parameter_types.len() == 1); for _ in 1..argument_types.len() { parameter_types.push(parameter_types[0].clone()); } } for (idx, ((parameter_span, parameter_type), argument_type)) in parameter_types.iter().zip(argument_types).enumerate() { match (parameter_type, argument_type) { (Type::Dimension(parameter_type), Type::Dimension(argument_type)) => { let mut parameter_type = substitute(&substitutions, parameter_type); let remaining_generic_subtypes: Vec<_> = parameter_type .iter() .filter(|BaseRepresentationFactor(name, _)| { type_parameters.iter().any(|(_, n)| name == n) }) .collect(); if remaining_generic_subtypes.len() > 1 { return Err(TypeCheckError::MultipleUnresolvedTypeParameters( *span, *parameter_span, )); } if let Some(&generic_subtype_factor) = remaining_generic_subtypes.first() { let generic_subtype = DType::from_factor(generic_subtype_factor.clone()); // The type of the idx-th parameter of the called function has a generic type // parameter inside. We can now instantiate that generic parameter by solving // the equation "parameter_type == argument_type" for the generic parameter. // In order to do this, let's assume `generic_subtype = D^alpha`, then we have // // parameter_type == argument_type // parameter_type / generic_subtype * D^alpha == argument_type // D^alpha == argument_type / (parameter_type / generic_subtype) // D == [argument_type / (parameter_type / generic_subtype)]^(1/alpha) // let alpha = Rational::from_integer(1) / generic_subtype_factor.1; let d = (argument_type.clone() / (parameter_type.clone() / generic_subtype)) .power(alpha); // We can now substitute that generic parameter in all subsequent expressions substitutions.push((generic_subtype_factor.0.clone(), d)); parameter_type = substitute(&substitutions, ¶meter_type); } if parameter_type != argument_type { return Err(TypeCheckError::IncompatibleDimensions( IncompatibleDimensionsError { span_operation: *span, operation: format!( "argument {num} of function call to '{name}'", num = idx + 1, name = function_name ), span_expected: parameter_types[idx].0, expected_name: "parameter type", expected_dimensions: self .registry .get_derived_entry_names_for(¶meter_type), expected_type: parameter_type, span_actual: arguments[idx].full_span(), actual_name: " argument type", actual_name_for_fix: "function argument", actual_dimensions: self .registry .get_derived_entry_names_for(&argument_type), actual_type: argument_type, }, )); } } (parameter_type, argument_type) => { if parameter_type != &argument_type { return Err(TypeCheckError::IncompatibleTypesInFunctionCall( Some(*parameter_span), parameter_type.clone(), arguments[idx].full_span(), argument_type.clone(), )); } } } } if substitutions.len() != type_parameters.len() { let parameters: HashSet = type_parameters .iter() .map(|(_, name)| name) .cloned() .collect(); let inferred_parameters: HashSet = substitutions.iter().map(|t| t.0.clone()).collect(); let remaining: Vec<_> = (¶meters - &inferred_parameters) .iter() .cloned() .collect(); return Err(TypeCheckError::CanNotInferTypeParameters( *span, *definition_span, function_name.into(), remaining.join(", "), )); } let return_type = match return_type { Type::Dimension(d) => Type::Dimension(substitute(&substitutions, d)), type_ => type_.clone(), }; Ok(typed_ast::Expression::FunctionCall( *span, *full_span, function_name.into(), arguments, return_type, )) } pub(crate) fn check_expression(&self, ast: &ast::Expression) -> Result { Ok(match ast { ast::Expression::Scalar(span, n) => typed_ast::Expression::Scalar(*span, *n), ast::Expression::Identifier(span, name) => { let type_ = self.identifier_type(*span, name)?.clone(); typed_ast::Expression::Identifier(*span, name.clone(), type_) } ast::Expression::UnitIdentifier(span, prefix, name, full_name) => { let type_ = self.identifier_type(*span, name)?.clone(); typed_ast::Expression::UnitIdentifier( *span, *prefix, name.clone(), full_name.clone(), type_, ) } ast::Expression::UnaryOperator { op, expr, span_op } => { let checked_expr = self.check_expression(expr)?; let type_ = checked_expr.get_type(); match (&type_, op) { (Type::Dimension(dtype), ast::UnaryOperator::Factorial) => { if !dtype.is_scalar() { return Err(TypeCheckError::NonScalarFactorialArgument( expr.full_span(), dtype.clone(), )); } } (Type::Dimension(_), ast::UnaryOperator::Negate) => (), (Type::Boolean, ast::UnaryOperator::LogicalNeg) => (), (_, ast::UnaryOperator::LogicalNeg) => { return Err(TypeCheckError::ExpectedBool(expr.full_span())) } _ => { return Err(TypeCheckError::ExpectedDimensionType( checked_expr.full_span(), type_.clone(), )); } }; typed_ast::Expression::UnaryOperator(*span_op, *op, Box::new(checked_expr), type_) } ast::Expression::BinaryOperator { op, lhs, rhs, span_op, } => { let lhs_checked = self.check_expression(lhs)?; let rhs_checked = self.check_expression(rhs)?; if let Type::Fn(parameter_types, return_type) = rhs_checked.get_type() { // make sure that there is just one paramter (return arity error otherwise) if parameter_types.len() != 1 { return Err(TypeCheckError::WrongArity { callable_span: rhs.full_span(), callable_name: "function".into(), callable_definition_span: None, arity: 1..=1, num_args: parameter_types.len(), }); } if parameter_types[0] != lhs_checked.get_type() { return Err(TypeCheckError::IncompatibleTypesInFunctionCall( None, parameter_types[0].clone(), lhs.full_span(), lhs_checked.get_type(), )); } typed_ast::Expression::CallableCall( lhs.full_span(), Box::new(rhs_checked), vec![lhs_checked], *return_type, ) } else if lhs_checked.get_type() == Type::DateTime { // DateTime types need special handling here, since they're not scalars with dimensions, // yet some select binary operators can be applied to them let rhs_is_time = dtype(&rhs_checked) .ok() .map(|t| t.is_time_dimension()) .unwrap_or(false); let rhs_is_datetime = rhs_checked.get_type() == Type::DateTime; if *op == BinaryOperator::Sub && rhs_is_datetime { let time = self .registry .get_base_representation_for_name("Time") .map_err(|_| { TypeCheckError::MissingDimension(ast.full_span(), "Time".into()) })?; // TODO make sure the "second" unit exists typed_ast::Expression::BinaryOperatorForDate( *span_op, *op, Box::new(lhs_checked), Box::new(rhs_checked), Type::Dimension(time), ) } else if (*op == BinaryOperator::Add || *op == BinaryOperator::Sub) && rhs_is_time { typed_ast::Expression::BinaryOperatorForDate( *span_op, *op, Box::new(lhs_checked), Box::new(rhs_checked), Type::DateTime, ) } else { return Err(TypeCheckError::IncompatibleTypesInOperator( span_op.unwrap_or_else(|| { ast::Expression::BinaryOperator { op: *op, lhs: lhs.clone(), rhs: rhs.clone(), span_op: *span_op, } .full_span() }), *op, lhs_checked.get_type(), lhs.full_span(), rhs_checked.get_type(), rhs.full_span(), )); } } else { let get_type_and_assert_equality = || { let lhs_type = dtype(&lhs_checked)?; let rhs_type = dtype(&rhs_checked)?; if lhs_type != rhs_type { let full_span = ast::Expression::BinaryOperator { op: *op, lhs: lhs.clone(), rhs: rhs.clone(), span_op: *span_op, } .full_span(); Err(TypeCheckError::IncompatibleDimensions( IncompatibleDimensionsError { span_operation: span_op.unwrap_or(full_span), operation: match op { typed_ast::BinaryOperator::Add => "addition".into(), typed_ast::BinaryOperator::Sub => "subtraction".into(), typed_ast::BinaryOperator::Mul => "multiplication".into(), typed_ast::BinaryOperator::Div => "division".into(), typed_ast::BinaryOperator::Power => "exponentiation".into(), typed_ast::BinaryOperator::ConvertTo => { "unit conversion".into() } typed_ast::BinaryOperator::LessThan | typed_ast::BinaryOperator::GreaterThan | typed_ast::BinaryOperator::LessOrEqual | typed_ast::BinaryOperator::GreaterOrEqual | typed_ast::BinaryOperator::Equal | typed_ast::BinaryOperator::NotEqual => { "comparison".into() } typed_ast::BinaryOperator::LogicalAnd => "and".into(), typed_ast::BinaryOperator::LogicalOr => "or".into(), }, span_expected: lhs.full_span(), expected_name: " left hand side", expected_dimensions: self .registry .get_derived_entry_names_for(&lhs_type), expected_type: lhs_type, span_actual: rhs.full_span(), actual_name: "right hand side", actual_name_for_fix: "expression on the right hand side", actual_dimensions: self .registry .get_derived_entry_names_for(&rhs_type), actual_type: rhs_type, }, )) } else { Ok(Type::Dimension(lhs_type)) } }; let type_ = match op { typed_ast::BinaryOperator::Add => get_type_and_assert_equality()?, typed_ast::BinaryOperator::Sub => get_type_and_assert_equality()?, typed_ast::BinaryOperator::Mul => { Type::Dimension(dtype(&lhs_checked)? * dtype(&rhs_checked)?) } typed_ast::BinaryOperator::Div => { Type::Dimension(dtype(&lhs_checked)? / dtype(&rhs_checked)?) } typed_ast::BinaryOperator::Power => { let exponent_type = dtype(&rhs_checked)?; if !exponent_type.is_scalar() { return Err(TypeCheckError::NonScalarExponent( rhs.full_span(), exponent_type, )); } let base_type = dtype(&lhs_checked)?; if base_type.is_scalar() { // Skip evaluating the exponent if the lhs is a scalar. This allows // for arbitrary (decimal) exponents, if the base is a scalar. Type::Dimension(base_type) } else { let exponent = evaluate_const_expr(&rhs_checked)?; Type::Dimension(base_type.power(exponent)) } } typed_ast::BinaryOperator::ConvertTo => get_type_and_assert_equality()?, typed_ast::BinaryOperator::LessThan | typed_ast::BinaryOperator::GreaterThan | typed_ast::BinaryOperator::LessOrEqual | typed_ast::BinaryOperator::GreaterOrEqual => { let _ = get_type_and_assert_equality()?; Type::Boolean } typed_ast::BinaryOperator::Equal | typed_ast::BinaryOperator::NotEqual => { let lhs_type = lhs_checked.get_type(); let rhs_type = rhs_checked.get_type(); if lhs_type.is_dtype() || rhs_type.is_dtype() { let _ = get_type_and_assert_equality()?; } else if lhs_type != rhs_type { return Err(TypeCheckError::IncompatibleTypesInComparison( span_op.unwrap(), lhs_type, lhs.full_span(), rhs_type, rhs.full_span(), )); } Type::Boolean } typed_ast::BinaryOperator::LogicalAnd | typed_ast::BinaryOperator::LogicalOr => { let lhs_type = lhs_checked.get_type(); let rhs_type = rhs_checked.get_type(); if lhs_type != Type::Boolean { return Err(TypeCheckError::ExpectedBool(lhs.full_span())); } else if rhs_type != Type::Boolean { return Err(TypeCheckError::ExpectedBool(rhs.full_span())); } Type::Boolean } }; typed_ast::Expression::BinaryOperator( *span_op, *op, Box::new(lhs_checked), Box::new(rhs_checked), type_, ) } } ast::Expression::FunctionCall(span, full_span, callable, args) => { let arguments_checked = args .iter() .map(|a| self.check_expression(a)) .collect::>>()?; let argument_types = arguments_checked .iter() .map(|e| e.get_type()) .collect::>(); // There are two options here. The 'callable' can either be a direct reference // to a (proper) function, or it can be an arbitrary complicated expression // that evaluates to a function "pointer". if let Some((name, signature)) = self.get_proper_function_reference(callable) { self.proper_function_call( span, full_span, &name, signature, arguments_checked, argument_types, )? } else { let callable_checked = self.check_expression(callable)?; let callable_type = callable_checked.get_type(); match callable_type { Type::Fn(parameters_types, return_type) => { let num_parameters = parameters_types.len(); let num_arguments = arguments_checked.len(); if num_parameters != num_arguments { return Err(TypeCheckError::WrongArity { callable_span: *span, callable_name: "function".into(), callable_definition_span: None, arity: num_parameters..=num_parameters, num_args: num_arguments, }); } for (param_type, arg_checked) in parameters_types.iter().zip(&arguments_checked) { if param_type != &arg_checked.get_type() { return Err(TypeCheckError::IncompatibleTypesInFunctionCall( None, param_type.clone(), arg_checked.full_span(), arg_checked.get_type(), )); } } typed_ast::Expression::CallableCall( *full_span, Box::new(callable_checked), arguments_checked, *return_type, ) } _ => { return Err(TypeCheckError::OnlyFunctionsAndReferencesCanBeCalled( callable.full_span(), )); } } } } ast::Expression::Boolean(span, val) => typed_ast::Expression::Boolean(*span, *val), ast::Expression::String(span, parts) => typed_ast::Expression::String( *span, parts .iter() .map(|p| match p { StringPart::Fixed(s) => Ok(typed_ast::StringPart::Fixed(s.clone())), StringPart::Interpolation(span, expr) => { Ok(typed_ast::StringPart::Interpolation( *span, Box::new(self.check_expression(expr)?), )) } }) .collect::>()?, ), ast::Expression::Condition(span, condition, then, else_) => { let condition = self.check_expression(condition)?; if condition.get_type() != Type::Boolean { return Err(TypeCheckError::ExpectedBool(condition.full_span())); } let then = self.check_expression(then)?; let else_ = self.check_expression(else_)?; let then_type = then.get_type(); let else_type = else_.get_type(); if then_type != else_type { return Err(TypeCheckError::IncompatibleTypesInCondition( *span, then_type, then.full_span(), else_type, else_.full_span(), )); } typed_ast::Expression::Condition( *span, Box::new(condition), Box::new(then), Box::new(else_), ) } }) } pub fn check_statement(&mut self, ast: &ast::Statement) -> Result { Ok(match ast { ast::Statement::Expression(expr) => { let checked_expr = self.check_expression(expr)?; for &identifier in LAST_RESULT_IDENTIFIERS { self.identifiers .insert(identifier.into(), (checked_expr.get_type(), None)); } typed_ast::Statement::Expression(checked_expr) } ast::Statement::DefineVariable { identifier_span, identifier, expr, type_annotation, decorators, } => { // Make sure that identifier does not clash with a function name. We do not // check for clashes with unit names, as this is handled by the prefix parser. if let Some(signature) = self.function_signatures.get(identifier) { return Err(TypeCheckError::NameAlreadyUsedBy( "a function", *identifier_span, Some(signature.definition_span), )); } let expr_checked = self.check_expression(expr)?; let type_deduced = expr_checked.get_type(); if let Some(ref type_annotation) = type_annotation { let type_annotated = self.type_from_annotation(type_annotation)?; match (&type_deduced, type_annotated) { (Type::Dimension(dexpr_deduced), Type::Dimension(dexpr_specified)) => { if dexpr_deduced != &dexpr_specified { return Err(TypeCheckError::IncompatibleDimensions( IncompatibleDimensionsError { span_operation: *identifier_span, operation: "variable definition".into(), span_expected: type_annotation.full_span(), expected_name: "specified dimension", expected_dimensions: self .registry .get_derived_entry_names_for(&dexpr_specified), expected_type: dexpr_specified, span_actual: expr.full_span(), actual_name: " actual dimension", actual_name_for_fix: "right hand side expression", actual_dimensions: self .registry .get_derived_entry_names_for(dexpr_deduced), actual_type: dexpr_deduced.clone(), }, )); } } (deduced, annotated) => { if deduced != &annotated { return Err(TypeCheckError::IncompatibleTypesInAnnotation( "definition".into(), *identifier_span, annotated, type_annotation.full_span(), deduced.clone(), expr_checked.full_span(), )); } } } } for (name, _) in decorator::name_and_aliases(identifier, decorators) { self.identifiers .insert(name.clone(), (type_deduced.clone(), Some(*identifier_span))); } typed_ast::Statement::DefineVariable( identifier.clone(), decorators.clone(), expr_checked, type_annotation .as_ref() .map(|d| d.pretty_print()) .unwrap_or_else(|| type_deduced.to_readable_type(&self.registry)), type_deduced, ) } ast::Statement::DefineBaseUnit(span, unit_name, type_annotation, decorators) => { let type_specified = if let Some(dexpr) = type_annotation { let base_representation = self .registry .get_base_representation(dexpr) .map_err(TypeCheckError::RegistryError)?; if base_representation.is_scalar() { return Err(TypeCheckError::NoDimensionlessBaseUnit( *span, unit_name.into(), )); } base_representation } else { use heck::ToUpperCamelCase; // In a unit definition like 'unit pixel' without a specified type, // we add a new type for the user let type_name = unit_name.to_upper_camel_case(); self.registry .add_base_dimension(&type_name) .map_err(TypeCheckError::RegistryError)? }; for (name, _) in decorator::name_and_aliases(unit_name, decorators) { self.identifiers.insert( name.clone(), (Type::Dimension(type_specified.clone()), Some(*span)), ); } typed_ast::Statement::DefineBaseUnit( unit_name.clone(), decorators.clone(), type_annotation .as_ref() .map(|d| d.pretty_print()) .unwrap_or_else(|| type_specified.to_readable_type(&self.registry)), Type::Dimension(type_specified), ) } ast::Statement::DefineDerivedUnit { identifier_span, identifier, expr, type_annotation_span, type_annotation, decorators, } => { // TODO: this is the *exact same code* that we have above for // variable definitions => deduplicate this somehow let expr_checked = self.check_expression(expr)?; let type_deduced = dtype(&expr_checked)?; if let Some(ref dexpr) = type_annotation { let type_specified = self .registry .get_base_representation(dexpr) .map_err(TypeCheckError::RegistryError)?; if type_deduced != type_specified { return Err(TypeCheckError::IncompatibleDimensions( IncompatibleDimensionsError { span_operation: *identifier_span, operation: "unit definition".into(), span_expected: type_annotation_span.unwrap(), expected_name: "specified dimension", expected_dimensions: self .registry .get_derived_entry_names_for(&type_specified), expected_type: type_specified, span_actual: expr.full_span(), actual_name: " actual dimension", actual_name_for_fix: "right hand side expression", actual_dimensions: self .registry .get_derived_entry_names_for(&type_deduced), actual_type: type_deduced, }, )); } } for (name, _) in decorator::name_and_aliases(identifier, decorators) { self.identifiers.insert( name.clone(), ( Type::Dimension(type_deduced.clone()), Some(*identifier_span), ), ); } typed_ast::Statement::DefineDerivedUnit( identifier.clone(), expr_checked, decorators.clone(), type_annotation .as_ref() .map(|d| d.pretty_print()) .unwrap_or_else(|| type_deduced.to_readable_type(&self.registry)), Type::Dimension(type_deduced), ) } ast::Statement::DefineFunction { function_name_span, function_name, type_parameters, parameters, body, return_type_annotation_span, return_type_annotation, } => { // Make sure that function name does not clash with an identifier. We do not // check for clashes with unit names, as this is handled by the prefix parser. if let Some((_, span)) = self.identifiers.get(function_name) { return Err(TypeCheckError::NameAlreadyUsedBy( "a constant", *function_name_span, *span, )); } if let Some(signature) = self.function_signatures.get(function_name) { if signature.is_foreign { return Err(TypeCheckError::NameAlreadyUsedBy( "a foreign function", *function_name_span, Some(signature.definition_span), )); } } let mut typechecker_fn = self.clone(); let is_ffi_function = body.is_none(); let mut type_parameters = type_parameters.clone(); for (span, type_parameter) in &type_parameters { match typechecker_fn.registry.add_base_dimension(type_parameter) { Err(RegistryError::EntryExists(name)) => { return Err(TypeCheckError::TypeParameterNameClash(*span, name)) } Err(err) => return Err(TypeCheckError::RegistryError(err)), _ => {} } } let mut typed_parameters = vec![]; let mut is_variadic = false; let mut free_type_parameters = vec![]; for (parameter_span, parameter, type_annotation, p_is_variadic) in parameters { let parameter_type = if let Some(type_annotation) = type_annotation { typechecker_fn.type_from_annotation(type_annotation)? } else if is_ffi_function { return Err(TypeCheckError::ForeignFunctionNeedsTypeAnnotations( *function_name_span, function_name.clone(), )); } else { let mut free_type_parameter = "".into(); for i in 0.. { free_type_parameter = format!("T{i}"); if !typechecker_fn.registry.contains(&free_type_parameter) { break; } } free_type_parameters.push((parameter.clone(), free_type_parameter.clone())); typechecker_fn .registry .add_base_dimension(&free_type_parameter) .expect("we selected a name that is free"); type_parameters.push((*parameter_span, free_type_parameter.clone())); Type::Dimension( typechecker_fn .registry .get_base_representation(&DimensionExpression::Dimension( *parameter_span, free_type_parameter, )) .map_err(TypeCheckError::RegistryError)?, ) }; typechecker_fn.identifiers.insert( parameter.clone(), (parameter_type.clone(), Some(*parameter_span)), ); typed_parameters.push(( *parameter_span, parameter.clone(), *p_is_variadic, type_annotation .as_ref() .map(|d| d.pretty_print()) .unwrap_or_else(|| parameter_type.to_readable_type(&self.registry)), parameter_type, )); is_variadic |= p_is_variadic; } if !free_type_parameters.is_empty() { // TODO: Perform type inference } let return_type_specified = return_type_annotation .as_ref() .map(|annotation| typechecker_fn.type_from_annotation(annotation)) .transpose()?; let add_function_signature = |tc: &mut TypeChecker, return_type: Type, is_foreign: bool| { let parameter_types = typed_parameters .iter() .map(|(span, _, _, _, t)| (*span, t.clone())) .collect(); tc.function_signatures.insert( function_name.clone(), FunctionSignature { definition_span: *function_name_span, type_parameters: type_parameters.clone(), parameter_types, is_variadic, return_type, is_foreign, }, ); }; if let Some(ref return_type_specified) = return_type_specified { // This is needed for recursive functions. If the return type // has been specified, we can already provide a function // signature before we check the body of the function. This // way, the 'typechecker_fn' can resolve the recursive call. add_function_signature( &mut typechecker_fn, return_type_specified.clone(), body.is_none(), ); } let body_checked = body .clone() .map(|expr| typechecker_fn.check_expression(&expr)) .transpose()?; let return_type = if let Some(ref expr) = body_checked { let type_deduced = expr.get_type(); if let Some(return_type_specified) = return_type_specified { match (type_deduced, return_type_specified) { (Type::Dimension(dtype_deduced), Type::Dimension(dtype_specified)) => { if dtype_deduced != dtype_specified { return Err(TypeCheckError::IncompatibleDimensions( IncompatibleDimensionsError { span_operation: *function_name_span, operation: "function return type".into(), span_expected: return_type_annotation_span.unwrap(), expected_name: "specified return type", expected_dimensions: self .registry .get_derived_entry_names_for(&dtype_specified), expected_type: dtype_specified, span_actual: body .as_ref() .map(|b| b.full_span()) .unwrap(), actual_name: " actual return type", actual_name_for_fix: "expression in the function body", actual_dimensions: self .registry .get_derived_entry_names_for(&dtype_deduced), actual_type: dtype_deduced, }, )); } Type::Dimension(dtype_deduced) } (type_deduced, type_specified) => { if type_deduced != type_specified { return Err(TypeCheckError::IncompatibleTypesInAnnotation( "function definition".into(), *function_name_span, type_specified, return_type_annotation_span.unwrap(), type_deduced, body.as_ref().map(|b| b.full_span()).unwrap(), )); } type_specified } } } else { type_deduced } } else { if !ffi::functions().contains_key(function_name.as_str()) { return Err(TypeCheckError::UnknownForeignFunction( *function_name_span, function_name.clone(), )); } return_type_specified.ok_or_else(|| { TypeCheckError::ForeignFunctionNeedsTypeAnnotations( *function_name_span, function_name.clone(), ) })? }; add_function_signature(self, return_type.clone(), body.is_none()); typed_ast::Statement::DefineFunction( function_name.clone(), type_parameters .iter() .map(|(_, name)| name.clone()) .collect(), typed_parameters, body_checked, return_type_annotation .as_ref() .map(|d| d.pretty_print()) .unwrap_or_else(|| return_type.to_readable_type(&self.registry)), return_type, ) } ast::Statement::DefineDimension(name, dexprs) => { if let Some(dexpr) = dexprs.first() { self.registry .add_derived_dimension(name, dexpr) .map_err(TypeCheckError::RegistryError)?; let base_representation = self .registry .get_base_representation_for_name(name) .expect("we just inserted it"); for alternative_expr in &dexprs[1..] { let alternative_base_representation = self .registry .get_base_representation(alternative_expr) .map_err(TypeCheckError::RegistryError)?; if alternative_base_representation != base_representation { return Err( TypeCheckError::IncompatibleAlternativeDimensionExpression( name.clone(), dexpr.full_span(), base_representation, alternative_expr.full_span(), alternative_base_representation, ), ); } } } else { self.registry .add_base_dimension(name) .map_err(TypeCheckError::RegistryError)?; } typed_ast::Statement::DefineDimension(name.clone(), dexprs.clone()) } ast::Statement::ProcedureCall(span, kind @ ProcedureKind::Type, args) => { if args.len() != 1 { return Err(TypeCheckError::WrongArity { callable_span: *span, callable_name: "type".into(), callable_definition_span: None, arity: 1..=1, num_args: args.len(), }); } let checked_args = args .iter() .map(|e| self.check_expression(e)) .collect::>>()?; typed_ast::Statement::ProcedureCall(kind.clone(), checked_args) } ast::Statement::ProcedureCall(span, kind, args) => { let procedure = ffi::procedures().get(kind).unwrap(); if !procedure.arity.contains(&args.len()) { return Err(TypeCheckError::WrongArity { callable_span: *span, callable_name: procedure.name.clone(), callable_definition_span: None, arity: procedure.arity.clone(), num_args: args.len(), }); } let checked_args = args .iter() .map(|e| self.check_expression(e)) .collect::>>()?; match kind { ProcedureKind::Print => { // no argument type checks required, everything can be printed } ProcedureKind::Assert => { if checked_args[0].get_type() != Type::Boolean { return Err(TypeCheckError::IncompatibleTypeInAssert( *span, checked_args[0].get_type(), checked_args[0].full_span(), )); } } ProcedureKind::AssertEq => { let type_first = dtype(&checked_args[0])?; for arg in &checked_args[1..] { let type_arg = dtype(arg)?; if type_arg != type_first { return Err(TypeCheckError::IncompatibleTypesInAssertEq( *span, checked_args[0].get_type(), checked_args[0].full_span(), arg.get_type(), arg.full_span(), )); } } } ProcedureKind::Type => { unreachable!("type() calls have a special handling above") } } typed_ast::Statement::ProcedureCall(kind.clone(), checked_args) } ast::Statement::ModuleImport(_, _) => { unreachable!("Modules should have been inlined by now") } }) } pub fn check_statements( &mut self, statements: impl IntoIterator, ) -> Result> { let mut statements_checked = vec![]; for statement in statements.into_iter() { statements_checked.push(self.check_statement(&statement)?); } Ok(statements_checked) } pub(crate) fn registry(&self) -> &DimensionRegistry { &self.registry } fn type_from_annotation(&self, annotation: &TypeAnnotation) -> Result { match annotation { TypeAnnotation::DimensionExpression(dexpr) => self .registry .get_base_representation(dexpr) .map(Type::Dimension) .map_err(TypeCheckError::RegistryError), TypeAnnotation::Bool(_) => Ok(Type::Boolean), TypeAnnotation::String(_) => Ok(Type::String), TypeAnnotation::DateTime(_) => Ok(Type::DateTime), TypeAnnotation::Fn(_, param_types, return_type) => Ok(Type::Fn( param_types .iter() .map(|p| self.type_from_annotation(p)) .collect::>>()?, Box::new(self.type_from_annotation(return_type)?), )), } } } #[cfg(test)] mod tests { use crate::parser::parse; use crate::prefix_transformer::Transformer; use super::*; const TEST_PRELUDE: &str = " dimension Scalar = 1 dimension A dimension B dimension C = A * B unit a: A unit b: B unit c: C = a * b fn returns_a() -> A = a fn takes_a_returns_a(x: A) -> A = x fn takes_a_returns_b(x: A) -> B = b fn takes_a_and_b_returns_c(x: A, y: B) -> C = x * y "; fn base_type(name: &str) -> BaseRepresentation { BaseRepresentation::from_factor(BaseRepresentationFactor( name.into(), Rational::from_integer(1), )) } fn type_a() -> BaseRepresentation { base_type("A") } fn type_b() -> BaseRepresentation { base_type("B") } fn type_c() -> BaseRepresentation { type_a() * type_b() } fn run_typecheck(input: &str) -> Result { let code = &format!("{prelude}\n{input}", prelude = TEST_PRELUDE, input = input); let statements = parse(code, 0).expect("No parse errors for inputs in this test suite"); let transformed_statements = Transformer::new() .transform(statements) .expect("No name resolution errors for inputs in this test suite"); TypeChecker::default() .check_statements(transformed_statements) .map(|mut statements_checked| statements_checked.pop().unwrap()) } fn assert_successful_typecheck(input: &str) { assert!(run_typecheck(input).is_ok()); } fn get_typecheck_error(input: &str) -> TypeCheckError { if let Err(err) = dbg!(run_typecheck(input)) { err } else { panic!("Input was expected to yield a type check error"); } } #[test] fn basic_arithmetic() { assert_successful_typecheck("2 a + a"); assert_successful_typecheck("2 a - a"); assert_successful_typecheck("a * b"); assert_successful_typecheck("a / b"); assert_successful_typecheck("a * b + 2 c"); assert_successful_typecheck("c / a + b"); assert!(matches!( get_typecheck_error("a + b"), TypeCheckError::IncompatibleDimensions(IncompatibleDimensionsError {expected_type, actual_type, ..}) if expected_type == type_a() && actual_type == type_b() )); } #[test] fn power_operator_with_scalar_base() { assert_successful_typecheck("2^2"); assert_successful_typecheck("2^(2^2)"); assert!(matches!( get_typecheck_error("2^a"), TypeCheckError::NonScalarExponent(_, t) if t == type_a() )); assert!(matches!( get_typecheck_error("2^(c/b)"), TypeCheckError::NonScalarExponent(_, t) if t == type_a() )); } #[test] fn power_operator_with_dimensionful_base() { assert_successful_typecheck("a^2"); assert_successful_typecheck("a^(2+3)"); assert_successful_typecheck("a^(2-3)"); assert_successful_typecheck("a^(2*3)"); assert_successful_typecheck("a^(2/3)"); assert_successful_typecheck("a^(2^3)"); assert!(matches!( get_typecheck_error("a^b"), TypeCheckError::NonScalarExponent(_, t) if t == type_b() )); // TODO: if we add ("constexpr") constants later, it would be great to support those in exponents. assert!(matches!( get_typecheck_error("let x=2 a^x"), TypeCheckError::UnsupportedConstEvalExpression(_, desc) if desc == "variable" )); assert!(matches!( get_typecheck_error("a^(3/(1-1))"), TypeCheckError::DivisionByZeroInConstEvalExpression(_) )); } #[test] fn converisons() { assert_successful_typecheck("2 a > a"); assert_successful_typecheck("2 a / (3 a) > 3"); assert!(matches!( get_typecheck_error("a > b"), TypeCheckError::IncompatibleDimensions(..) )); } #[test] fn variable_definitions() { assert_successful_typecheck( "let x: A = a let y: B = b", ); assert_successful_typecheck("let x: C = a * b"); assert_successful_typecheck("let x: C = 2 * a * b^2 / b"); assert_successful_typecheck("let x: A^3 = a^20 * a^(-17)"); assert_successful_typecheck("let x: A = c / b"); assert_successful_typecheck("let x: Bool = true"); assert_successful_typecheck("let x: String = \"hello\""); assert!(matches!( get_typecheck_error("let x: A = b"), TypeCheckError::IncompatibleDimensions(IncompatibleDimensionsError {expected_type, actual_type, ..}) if expected_type == type_a() && actual_type == type_b() )); assert!(matches!( get_typecheck_error("let x: A = true"), TypeCheckError::IncompatibleTypesInAnnotation(_, _, annotated_type, _, actual_type, _) if annotated_type == Type::Dimension(type_a()) && actual_type == Type::Boolean )); assert!(matches!( get_typecheck_error("let x: A = \"foo\""), TypeCheckError::IncompatibleTypesInAnnotation(_, _, annotated_type, _, actual_type, _) if annotated_type == Type::Dimension(type_a()) && actual_type == Type::String )); assert!(matches!( get_typecheck_error("let x: Bool = a"), TypeCheckError::IncompatibleTypesInAnnotation(_, _, annotated_type, _, actual_type, _) if annotated_type == Type::Boolean && actual_type == Type::Dimension(type_a()) )); assert!(matches!( get_typecheck_error("let x: String = true"), TypeCheckError::IncompatibleTypesInAnnotation(_, _, annotated_type, _, actual_type, _) if annotated_type == Type::String && actual_type == Type::Boolean )); } #[test] fn unit_definitions() { assert_successful_typecheck("unit my_c: C = a * b"); assert_successful_typecheck("unit foo: A*B^2 = a b^2"); assert!(matches!( get_typecheck_error("unit my_c: C = a"), TypeCheckError::IncompatibleDimensions(IncompatibleDimensionsError {expected_type, actual_type, ..}) if expected_type == type_c() && actual_type == type_a() )); } #[test] fn function_definitions() { assert_successful_typecheck("fn f(x: A) -> A = x"); assert_successful_typecheck("fn f(x: A) -> A·B = 2 * x * b"); assert_successful_typecheck("fn f(x: A, y: B) -> C = x * y"); assert_successful_typecheck("fn f(x: A) = x"); assert!(matches!( get_typecheck_error("fn f(x: A, y: B) -> C = x / y"), TypeCheckError::IncompatibleDimensions(IncompatibleDimensionsError {expected_type, actual_type, ..}) if expected_type == type_c() && actual_type == type_a() / type_b() )); assert!(matches!( get_typecheck_error("fn f(x: A) -> A = a\n\ f(b)"), TypeCheckError::IncompatibleDimensions(IncompatibleDimensionsError {expected_type, actual_type, ..}) if expected_type == type_a() && actual_type == type_b() )); } #[test] fn recursive_functions() { assert_successful_typecheck("fn f(x: Scalar) -> Scalar = if x < 0 then f(-x) else x"); assert_successful_typecheck( "fn factorial(n: Scalar) -> Scalar = if n < 0 then 1 else factorial(n - 1) * n", ); assert!(matches!( get_typecheck_error("fn f(x: Scalar) -> A = if x < 0 then f(-x) else 2 b"), TypeCheckError::IncompatibleTypesInCondition(_, lhs, _, rhs, _) if lhs == Type::Dimension(type_a()) && rhs == Type::Dimension(type_b()) )); } #[test] fn generics_basic() { assert_successful_typecheck( " fn f(x: D) -> D = x f(2) f(2 a) ", ); assert_successful_typecheck( " fn f(x: D) -> D^2 = x*x f(2) f(2 a) ", ); assert_successful_typecheck( " fn f(x: D0, y: D1) -> D0/D1^2 = x/y^2 f(2, 3) f(2 a, 2 b) ", ); assert!(matches!( get_typecheck_error("fn f(x: T1, y: T2) -> T2/T1 = x/y"), TypeCheckError::IncompatibleDimensions(IncompatibleDimensionsError {expected_type, actual_type, ..}) if expected_type == base_type("T2") / base_type("T1") && actual_type == base_type("T1") / base_type("T2") )); } #[test] fn generics_multiple_unresolved_type_parameters() { assert!(matches!( get_typecheck_error( " fn foo(x: D1*D2) = 1 foo(2) " ), TypeCheckError::MultipleUnresolvedTypeParameters(..) )); } #[test] fn generics_unused_type_parameter() { assert!(matches!( get_typecheck_error(" fn foo(x: Scalar) -> Scalar = 1 foo(2) "), TypeCheckError::CanNotInferTypeParameters(_, _, function_name, parameters) if function_name == "foo" && parameters == "D0" )); assert!(matches!( get_typecheck_error(" fn foo(x: D0, y: D0) -> Scalar = 1 foo(2, 3) "), TypeCheckError::CanNotInferTypeParameters(_, _, function_name, parameters) if function_name == "foo" && parameters == "D1" )); assert!(matches!( get_typecheck_error(" fn foo(x: Scalar, y: Scalar) -> Scalar = 1 foo(2, 3) "), TypeCheckError::CanNotInferTypeParameters(_, _, function_name, parameters) if function_name == "foo" && (parameters == "D1, D0" || parameters == "D0, D1") )); } #[test] fn generics_type_parameter_name_clash() { assert!(matches!( get_typecheck_error(" dimension Existing fn f(x: Existing) = 1 "), TypeCheckError::TypeParameterNameClash(_, name) if name == "Existing" )); } #[test] fn unknown_identifier() { assert!(matches!( get_typecheck_error("a + d"), TypeCheckError::UnknownIdentifier(_, ident, _) if ident == "d" )); } #[test] fn unknown_function() { assert!(matches!( get_typecheck_error("foo(2)"), TypeCheckError::UnknownIdentifier(_, name, _) if name == "foo" )); } #[test] fn incompatible_alternative_dimension_expression() { assert!(matches!( get_typecheck_error( "# wrong alternative expression: C / B^2 dimension D = A / B = C / B^3" ), TypeCheckError::IncompatibleAlternativeDimensionExpression(t, ..) if t == "D", )); } #[test] fn wrong_arity() { assert!(matches!( get_typecheck_error(" fn f() = 1 f(1) "), TypeCheckError::WrongArity{callable_span:_, callable_name, callable_definition_span: _, arity, num_args: 1} if arity == (0..=0) && callable_name == "f" )); assert!(matches!( get_typecheck_error(" fn f(x: Scalar) = x f() "), TypeCheckError::WrongArity{callable_span:_, callable_name, callable_definition_span: _, arity, num_args: 0} if arity == (1..=1) && callable_name == "f" )); assert!(matches!( get_typecheck_error(" fn f(x: Scalar) = x f(2, 3) "), TypeCheckError::WrongArity{callable_span:_, callable_name, callable_definition_span: _, arity, num_args: 2} if arity == (1..=1) && callable_name == "f" )); assert!(matches!( get_typecheck_error(" fn mean(xs: D…) -> D mean() "), TypeCheckError::WrongArity{callable_span:_, callable_name, callable_definition_span: _, arity, num_args: 0} if arity == (1..=usize::MAX) && callable_name == "mean" )); } #[test] fn variadic_functions() { assert!(matches!( get_typecheck_error( " fn mean(xs: D…) -> D mean(1 a, 1 b) " ), TypeCheckError::IncompatibleDimensions { .. } )); } #[test] fn foreign_function_with_missing_return_type() { assert!(matches!( get_typecheck_error("fn sin(x: Scalar)"), TypeCheckError::ForeignFunctionNeedsTypeAnnotations(_, name) if name == "sin" )); } #[test] fn unknown_foreign_function() { assert!(matches!( get_typecheck_error("fn foo(x: Scalar) -> Scalar"), TypeCheckError::UnknownForeignFunction(_, name) if name == "foo" )); } #[test] fn arity_checks_in_procedure_calls() { assert!(matches!( get_typecheck_error("assert_eq(1)"), TypeCheckError::WrongArity{callable_span:_, callable_name, callable_definition_span: _, arity, num_args: 1} if arity == (2..=3) && callable_name == "assert_eq" )); assert_successful_typecheck("assert_eq(1,2)"); assert_successful_typecheck("assert_eq(1,2,3)"); assert!(matches!( get_typecheck_error("assert_eq(1,2,3,4)"), TypeCheckError::WrongArity{callable_span:_, callable_name, callable_definition_span: _, arity, num_args: 4} if arity == (2..=3) && callable_name == "assert_eq" )); } #[test] fn boolean_values() { assert!(matches!( get_typecheck_error("-true"), TypeCheckError::ExpectedDimensionType(_, _) )); } #[test] fn conditionals() { assert_successful_typecheck("if true then 1 else 2"); assert_successful_typecheck("if true then true else false"); assert!(matches!( get_typecheck_error("if 1 then 2 else 3"), TypeCheckError::ExpectedBool(_) )); assert!(matches!( get_typecheck_error("if true then a else b"), TypeCheckError::IncompatibleTypesInCondition(_, t1, _, t2, _) if t1 == Type::Dimension(base_type("A")) && t2 == Type::Dimension(base_type("B")) )); assert!(matches!( get_typecheck_error("if true then true else a"), TypeCheckError::IncompatibleTypesInCondition(_, t1, _, t2, _) if t1 == Type::Boolean && t2 == Type::Dimension(base_type("A")) )); } #[test] fn non_dtype_return_types() { assert!(matches!( get_typecheck_error("fn f() -> String = 1"), TypeCheckError::IncompatibleTypesInAnnotation(..) )); assert!(matches!( get_typecheck_error("fn f() -> Scalar = \"test\""), TypeCheckError::IncompatibleTypesInAnnotation(..) )); assert!(matches!( get_typecheck_error("fn f() -> Bool = 1"), TypeCheckError::IncompatibleTypesInAnnotation(..) )); assert!(matches!( get_typecheck_error("fn f() -> Scalar = true"), TypeCheckError::IncompatibleTypesInAnnotation(..) )); assert!(matches!( get_typecheck_error("fn f() -> String = true"), TypeCheckError::IncompatibleTypesInAnnotation(..) )); assert!(matches!( get_typecheck_error("fn f() -> Bool = \"test\""), TypeCheckError::IncompatibleTypesInAnnotation(..) )); } #[test] fn function_types_basic() { assert_successful_typecheck( " let returns_a_ref1 = returns_a let returns_a_ref2: Fn[() -> A] = returns_a let takes_a_returns_a_ref1 = takes_a_returns_a let takes_a_returns_a_ref2: Fn[(A) -> A] = takes_a_returns_a let takes_a_returns_b_ref1 = takes_a_returns_b let takes_a_returns_b_ref2: Fn[(A) -> B] = takes_a_returns_b let takes_a_and_b_returns_C_ref1 = takes_a_and_b_returns_c let takes_a_and_b_returns_C_ref2: Fn[(A, B) -> C] = takes_a_and_b_returns_c let takes_a_and_b_returns_C_ref3: Fn[(A, B) -> A × B] = takes_a_and_b_returns_c ", ); assert!(matches!( get_typecheck_error("let wrong_return_type: Fn[() -> B] = returns_a"), TypeCheckError::IncompatibleTypesInAnnotation(..) )); assert!(matches!( get_typecheck_error("let wrong_argument_type: Fn[(B) -> A] = takes_a_returns_a"), TypeCheckError::IncompatibleTypesInAnnotation(..) )); assert!(matches!( get_typecheck_error("let wrong_argument_count: Fn[(A, B) -> C] = takes_a_returns_a"), TypeCheckError::IncompatibleTypesInAnnotation(..) )); } #[test] fn function_types_in_return_position() { assert_successful_typecheck( " fn returns_fn1() -> Fn[() -> A] = returns_a fn returns_fn2() -> Fn[(A) -> A] = takes_a_returns_a fn returns_fn3() -> Fn[(A) -> B] = takes_a_returns_b fn returns_fn4() -> Fn[(A, B) -> C] = takes_a_and_b_returns_c ", ); assert!(matches!( get_typecheck_error("fn returns_fn5() -> Fn[() -> B] = returns_a"), TypeCheckError::IncompatibleTypesInAnnotation(..) )); } #[test] fn function_types_in_argument_position() { assert_successful_typecheck( " fn takes_fn1(f: Fn[() -> A]) -> A = f() fn takes_fn2(f: Fn[(A) -> A]) -> A = f(a) fn takes_fn3(f: Fn[(A) -> B]) -> B = f(a) fn takes_fn4(f: Fn[(A, B) -> C]) -> C = f(a, b) takes_fn1(returns_a) takes_fn2(takes_a_returns_a) takes_fn3(takes_a_returns_b) takes_fn4(takes_a_and_b_returns_c) ", ); assert!(matches!( get_typecheck_error( " fn wrong_arity(f: Fn[(A) -> B]) -> B = f() " ), TypeCheckError::WrongArity { .. } )); assert!(matches!( get_typecheck_error( " fn wrong_argument_type(f: Fn[(A) -> B]) -> B = f(b) " ), TypeCheckError::IncompatibleTypesInFunctionCall(..) )); assert!(matches!( get_typecheck_error( " fn wrong_return_type(f: Fn[() -> A]) -> B = f() " ), TypeCheckError::IncompatibleDimensions(..) )); assert!(matches!( get_typecheck_error( " fn argument_mismatch(f: Fn[() -> A]) -> A = f() argument_mismatch(takes_a_returns_a) " ), TypeCheckError::IncompatibleTypesInFunctionCall(..) )); } #[test] fn no_dimensionless_base_units() { assert!(matches!( get_typecheck_error( " unit page: Scalar " ), TypeCheckError::NoDimensionlessBaseUnit { .. } )); } } numbat-1.11.0/src/typed_ast.rs000064400000000000000000000722221046102023000143300ustar 00000000000000use itertools::Itertools; use crate::arithmetic::{Exponent, Rational}; use crate::ast::ProcedureKind; pub use crate::ast::{BinaryOperator, DimensionExpression, UnaryOperator}; use crate::dimension::DimensionRegistry; use crate::{ decorator::Decorator, markup::Markup, number::Number, prefix::Prefix, prefix_parser::AcceptsPrefix, pretty_print::PrettyPrint, registry::BaseRepresentation, span::Span, }; use crate::{markup as m, BaseRepresentationFactor}; /// Dimension type pub type DType = BaseRepresentation; impl DType { pub fn is_scalar(&self) -> bool { self == &DType::unity() } pub fn to_readable_type(&self, registry: &DimensionRegistry) -> m::Markup { if self.is_scalar() { return m::type_identifier("Scalar"); } let mut names = vec![]; let factors: Vec<_> = self.iter().collect(); if factors.len() == 1 && factors[0].1 == Exponent::from_integer(1) { names.push(factors[0].0.clone()); } names.extend(registry.get_derived_entry_names_for(self)); match &names[..] { [] => self.pretty_print(), [single] => m::type_identifier(single), multiple => { Itertools::intersperse(multiple.iter().map(m::type_identifier), m::dimmed(" or ")) .sum() } } } /// Is the current dimension type the Time dimension? /// /// This is special helper that's useful when dealing with DateTimes pub fn is_time_dimension(&self) -> bool { *self == BaseRepresentation::from_factor(BaseRepresentationFactor( "Time".into(), Rational::from_integer(1), )) } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Type { Dimension(DType), Boolean, String, DateTime, Fn(Vec, Box), } impl std::fmt::Display for Type { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Type::Dimension(d) => d.fmt(f), Type::Boolean => write!(f, "Bool"), Type::String => write!(f, "String"), Type::DateTime => write!(f, "DateTime"), Type::Fn(param_types, return_type) => { write!( f, "Fn[({ps}) -> {return_type}]", ps = param_types.iter().map(|p| p.to_string()).join(", ") ) } } } } impl PrettyPrint for Type { fn pretty_print(&self) -> Markup { match self { Type::Dimension(d) => d.pretty_print(), Type::Boolean => m::keyword("Bool"), Type::String => m::keyword("String"), Type::DateTime => m::keyword("DateTime"), Type::Fn(param_types, return_type) => { m::type_identifier("Fn") + m::operator("[(") + Itertools::intersperse( param_types.iter().map(|t| t.pretty_print()), m::operator(",") + m::space(), ) .sum() + m::operator(")") + m::space() + m::operator("->") + m::space() + return_type.pretty_print() + m::operator("]") } } } } impl Type { pub fn to_readable_type(&self, registry: &DimensionRegistry) -> Markup { match self { Type::Dimension(d) => d.to_readable_type(registry), _ => self.pretty_print(), } } pub fn scalar() -> Type { Type::Dimension(DType::unity()) } pub fn is_dtype(&self) -> bool { matches!(self, Type::Dimension(..)) } pub fn is_fn_type(&self) -> bool { matches!(self, Type::Fn(..)) } } #[derive(Debug, Clone, PartialEq)] pub enum StringPart { Fixed(String), Interpolation(Span, Box), } impl PrettyPrint for StringPart { fn pretty_print(&self) -> Markup { match self { StringPart::Fixed(s) => s.pretty_print(), StringPart::Interpolation(_, expr) => { m::operator("{") + expr.pretty_print() + m::operator("}") } } } } impl PrettyPrint for &Vec { fn pretty_print(&self) -> Markup { m::operator("\"") + self.iter().map(|p| p.pretty_print()).sum() + m::operator("\"") } } #[derive(Debug, Clone, PartialEq)] pub enum Expression { Scalar(Span, Number), Identifier(Span, String, Type), UnitIdentifier(Span, Prefix, String, String, Type), UnaryOperator(Span, UnaryOperator, Box, Type), BinaryOperator( Option, BinaryOperator, Box, Box, Type, ), /// A special binary operator that has a DateTime as one (or both) of the operands BinaryOperatorForDate( Option, BinaryOperator, /// LHS must evaluate to a DateTime Box, /// RHS can evaluate to a DateTime or a quantity of type Time Box, Type, ), // A 'proper' function call FunctionCall(Span, Span, String, Vec, Type), // A call via a function object CallableCall(Span, Box, Vec, Type), Boolean(Span, bool), Condition(Span, Box, Box, Box), String(Span, Vec), } impl Expression { pub fn full_span(&self) -> Span { match self { Expression::Scalar(span, ..) => *span, Expression::Identifier(span, ..) => *span, Expression::UnitIdentifier(span, ..) => *span, Expression::UnaryOperator(span, _, expr, _) => span.extend(&expr.full_span()), Expression::BinaryOperator(span_op, _op, lhs, rhs, _) => { let mut span = lhs.full_span().extend(&rhs.full_span()); if let Some(span_op) = span_op { span = span.extend(span_op); } span } Expression::BinaryOperatorForDate(span_op, _op, lhs, rhs, ..) => { let mut span = lhs.full_span().extend(&rhs.full_span()); if let Some(span_op) = span_op { span = span.extend(span_op); } span } Expression::FunctionCall(_identifier_span, full_span, _, _, _) => *full_span, Expression::CallableCall(full_span, _, _, _) => *full_span, Expression::Boolean(span, _) => *span, Expression::Condition(span_if, _, _, then_expr) => { span_if.extend(&then_expr.full_span()) } Expression::String(span, _) => *span, } } } #[derive(Debug, Clone, PartialEq)] pub enum Statement { Expression(Expression), DefineVariable(String, Vec, Expression, Markup, Type), DefineFunction( String, Vec, // type parameters Vec<( // parameter: Span, // span of the parameter String, // parameter name bool, // whether or not it is variadic Markup, // readable parameter type Type, // parameter type )>, Option, // function body Markup, // readable return type Type, // return type ), DefineDimension(String, Vec), DefineBaseUnit(String, Vec, Markup, Type), DefineDerivedUnit(String, Expression, Vec, Markup, Type), ProcedureCall(crate::ast::ProcedureKind, Vec), } impl Statement { pub fn as_expression(&self) -> Option<&Expression> { if let Self::Expression(v) = self { Some(v) } else { None } } } impl Expression { pub fn get_type(&self) -> Type { match self { Expression::Scalar(_, _) => Type::Dimension(DType::unity()), Expression::Identifier(_, _, type_) => type_.clone(), Expression::UnitIdentifier(_, _, _, _, _type) => _type.clone(), Expression::UnaryOperator(_, _, _, type_) => type_.clone(), Expression::BinaryOperator(_, _, _, _, type_) => type_.clone(), Expression::BinaryOperatorForDate(_, _, _, _, type_, ..) => type_.clone(), Expression::FunctionCall(_, _, _, _, type_) => type_.clone(), Expression::CallableCall(_, _, _, type_) => type_.clone(), Expression::Boolean(_, _) => Type::Boolean, Expression::Condition(_, _, then, _) => then.get_type(), Expression::String(_, _) => Type::String, } } } fn accepts_prefix_markup(accepts_prefix: &Option) -> Markup { if let Some(accepts_prefix) = accepts_prefix { m::operator(":") + m::space() + match accepts_prefix { AcceptsPrefix { short: true, long: true, } => m::keyword("both"), AcceptsPrefix { short: true, long: false, } => m::keyword("short"), AcceptsPrefix { short: false, long: true, } => m::keyword("long"), AcceptsPrefix { short: false, long: false, } => m::keyword("none"), } } else { m::empty() } } fn decorator_markup(decorators: &Vec) -> Markup { let mut markup_decorators = m::empty(); for decorator in decorators { markup_decorators = markup_decorators + match decorator { Decorator::MetricPrefixes => m::decorator("@metric_prefixes"), Decorator::BinaryPrefixes => m::decorator("@binary_prefixes"), Decorator::Aliases(names) => { m::decorator("@aliases") + m::operator("(") + Itertools::intersperse( names.iter().map(|(name, accepts_prefix)| { m::unit(name) + accepts_prefix_markup(accepts_prefix) }), m::operator(", "), ) .sum() + m::operator(")") } Decorator::Url(url) => { m::decorator("@url") + m::operator("(") + m::string(url) + m::operator(")") } Decorator::Name(name) => { m::decorator("@name") + m::operator("(") + m::string(name) + m::operator(")") } } + m::nl(); } markup_decorators } impl PrettyPrint for Statement { fn pretty_print(&self) -> Markup { match self { Statement::DefineVariable(identifier, _decs, expr, readable_type, _type) => { m::keyword("let") + m::space() + m::identifier(identifier) + m::operator(":") + m::space() + readable_type.clone() + m::space() + m::operator("=") + m::space() + expr.pretty_print() } Statement::DefineFunction( function_name, type_parameters, parameters, body, readable_return_type, _return_type, ) => { let markup_type_parameters = if type_parameters.is_empty() { m::empty() } else { m::operator("<") + Itertools::intersperse( type_parameters.iter().map(m::type_identifier), m::operator(", "), ) .sum() + m::operator(">") }; let markup_parameters = Itertools::intersperse( parameters .iter() .map(|(_span, name, is_variadic, readable_type, _type)| { m::identifier(name) + m::operator(":") + m::space() + readable_type.clone() + if *is_variadic { m::operator("…") } else { m::empty() } }), m::operator(", "), ) .sum(); let markup_return_type = m::space() + m::operator("->") + m::space() + readable_return_type.clone(); m::keyword("fn") + m::space() + m::identifier(function_name) + markup_type_parameters + m::operator("(") + markup_parameters + m::operator(")") + markup_return_type + body .as_ref() .map(|e| m::space() + m::operator("=") + m::space() + e.pretty_print()) .unwrap_or_default() } Statement::Expression(expr) => expr.pretty_print(), Statement::DefineDimension(identifier, dexprs) if dexprs.is_empty() => { m::keyword("dimension") + m::space() + m::type_identifier(identifier) } Statement::DefineDimension(identifier, dexprs) => { m::keyword("dimension") + m::space() + m::type_identifier(identifier) + m::space() + m::operator("=") + m::space() + Itertools::intersperse( dexprs.iter().map(|d| d.pretty_print()), m::space() + m::operator("=") + m::space(), ) .sum() } Statement::DefineBaseUnit(identifier, decorators, readable_type, _type) => { decorator_markup(decorators) + m::keyword("unit") + m::space() + m::unit(identifier) + m::operator(":") + m::space() + readable_type.clone() } Statement::DefineDerivedUnit(identifier, expr, decorators, readable_type, _type) => { decorator_markup(decorators) + m::keyword("unit") + m::space() + m::unit(identifier) + m::operator(":") + m::space() + readable_type.clone() + m::space() + m::operator("=") + m::space() + expr.pretty_print() } Statement::ProcedureCall(kind, args) => { let identifier = match kind { ProcedureKind::Print => "print", ProcedureKind::Assert => "assert", ProcedureKind::AssertEq => "assert_eq", ProcedureKind::Type => "type", }; m::identifier(identifier) + m::operator("(") + Itertools::intersperse( args.iter().map(|a| a.pretty_print()), m::operator(",") + m::space(), ) .sum() + m::operator(")") } } } } fn pretty_scalar(n: Number) -> Markup { m::value(n.pretty_print()) } fn with_parens(expr: &Expression) -> Markup { match expr { Expression::Scalar(..) | Expression::Identifier(..) | Expression::UnitIdentifier(..) | Expression::FunctionCall(..) | Expression::CallableCall(..) | Expression::Boolean(..) | Expression::String(..) => expr.pretty_print(), Expression::UnaryOperator { .. } | Expression::BinaryOperator { .. } | Expression::BinaryOperatorForDate { .. } | Expression::Condition(..) => m::operator("(") + expr.pretty_print() + m::operator(")"), } } /// Add parens, if needed -- liberal version, can not be used for exponentiation. fn with_parens_liberal(expr: &Expression) -> Markup { match expr { Expression::BinaryOperator(_, BinaryOperator::Mul, lhs, rhs, _type) if matches!(**lhs, Expression::Scalar(..)) && matches!(**rhs, Expression::UnitIdentifier(..)) => { expr.pretty_print() } _ => with_parens(expr), } } fn pretty_print_binop(op: &BinaryOperator, lhs: &Expression, rhs: &Expression) -> Markup { match op { BinaryOperator::ConvertTo => { // never needs parens, it has the lowest precedence: lhs.pretty_print() + op.pretty_print() + rhs.pretty_print() } BinaryOperator::Mul => match (lhs, rhs) { ( Expression::Scalar(_, s), Expression::UnitIdentifier(_, prefix, _name, full_name, _type), ) => { // Fuse multiplication of a scalar and a unit to a quantity pretty_scalar(*s) + m::space() + m::unit(format!("{}{}", prefix.as_string_long(), full_name)) } (Expression::Scalar(_, s), Expression::Identifier(_, name, _type)) => { // Fuse multiplication of a scalar and identifier pretty_scalar(*s) + m::space() + m::identifier(name) } _ => { let add_parens_if_needed = |expr: &Expression| { if matches!( expr, Expression::BinaryOperator(_, BinaryOperator::Power, ..) | Expression::BinaryOperator(_, BinaryOperator::Mul, ..) ) { expr.pretty_print() } else { with_parens_liberal(expr) } }; add_parens_if_needed(lhs) + op.pretty_print() + add_parens_if_needed(rhs) } }, BinaryOperator::Div => { let lhs_add_parens_if_needed = |expr: &Expression| { if matches!( expr, Expression::BinaryOperator(_, BinaryOperator::Power, ..) | Expression::BinaryOperator(_, BinaryOperator::Mul, ..) ) { expr.pretty_print() } else { with_parens_liberal(expr) } }; let rhs_add_parens_if_needed = |expr: &Expression| { if matches!( expr, Expression::BinaryOperator(_, BinaryOperator::Power, ..) ) { expr.pretty_print() } else { with_parens_liberal(expr) } }; lhs_add_parens_if_needed(lhs) + op.pretty_print() + rhs_add_parens_if_needed(rhs) } BinaryOperator::Add => { let add_parens_if_needed = |expr: &Expression| { if matches!( expr, Expression::BinaryOperator(_, BinaryOperator::Power, ..) | Expression::BinaryOperator(_, BinaryOperator::Mul, ..) | Expression::BinaryOperator(_, BinaryOperator::Add, ..) ) { expr.pretty_print() } else { with_parens_liberal(expr) } }; add_parens_if_needed(lhs) + op.pretty_print() + add_parens_if_needed(rhs) } BinaryOperator::Sub => { let add_parens_if_needed = |expr: &Expression| { if matches!( expr, Expression::BinaryOperator(_, BinaryOperator::Power, ..) | Expression::BinaryOperator(_, BinaryOperator::Mul, ..) ) { expr.pretty_print() } else { with_parens_liberal(expr) } }; add_parens_if_needed(lhs) + op.pretty_print() + add_parens_if_needed(rhs) } BinaryOperator::Power if matches!(rhs, Expression::Scalar(_, n) if n.to_f64() == 2.0) => { with_parens(lhs) + m::operator("²") } BinaryOperator::Power if matches!(rhs, Expression::Scalar(_, n) if n.to_f64() == 3.0) => { with_parens(lhs) + m::operator("³") } _ => with_parens(lhs) + op.pretty_print() + with_parens(rhs), } } impl PrettyPrint for Expression { fn pretty_print(&self) -> Markup { use Expression::*; match self { Scalar(_, n) => pretty_scalar(*n), Identifier(_, name, _type) => m::identifier(name), UnitIdentifier(_, prefix, _name, full_name, _type) => { m::unit(format!("{}{}", prefix.as_string_long(), full_name)) } UnaryOperator(_, self::UnaryOperator::Negate, expr, _type) => { m::operator("-") + with_parens(expr) } UnaryOperator(_, self::UnaryOperator::Factorial, expr, _type) => { with_parens(expr) + m::operator("!") } UnaryOperator(_, self::UnaryOperator::LogicalNeg, expr, _type) => { m::operator("!") + with_parens(expr) } BinaryOperator(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs), BinaryOperatorForDate(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs), FunctionCall(_, _, name, args, _type) => { m::identifier(name) + m::operator("(") + itertools::Itertools::intersperse( args.iter().map(|e| e.pretty_print()), m::operator(",") + m::space(), ) .sum() + m::operator(")") } CallableCall(_, expr, args, _type) => { expr.pretty_print() + m::operator("(") + itertools::Itertools::intersperse( args.iter().map(|e| e.pretty_print()), m::operator(",") + m::space(), ) .sum() + m::operator(")") } Boolean(_, val) => val.pretty_print(), String(_, parts) => parts.pretty_print(), Condition(_, condition, then, else_) => { m::keyword("if") + m::space() + with_parens(condition) + m::space() + m::keyword("then") + m::space() + with_parens(then) + m::space() + m::keyword("else") + m::space() + with_parens(else_) } } } } #[cfg(test)] mod tests { use super::*; use crate::ast::ReplaceSpans; use crate::markup::{Formatter, PlainTextFormatter}; use crate::prefix_transformer::Transformer; fn parse(code: &str) -> Statement { let statements = crate::parser::parse( &format!( "dimension Scalar = 1 dimension Length dimension Time dimension Mass fn sin(x: Scalar) -> Scalar fn cos(x: Scalar) -> Scalar fn asin(x: Scalar) -> Scalar fn atan(x: Scalar) -> Scalar fn atan2(x: T, y: T) -> Scalar fn sqrt(x) = x^(1/2) let pi = 2 asin(1) @aliases(m: short) @metric_prefixes unit meter: Length @aliases(s: short) @metric_prefixes unit second: Time @aliases(g: short) @metric_prefixes unit gram: Mass @aliases(rad: short) @metric_prefixes unit radian: Scalar = meter / meter @aliases(°: none) unit degree = 180/pi × radian @aliases(in: short) unit inch = 0.0254 m @metric_prefixes unit points let a = 1 let b = 1 let c = 1 let d = 1 let e = 1 let f = 1 let x = 1 let r = 2 m let vol = 3 m^3 let density = 1000 kg / m^3 let länge = 1 let x_2 = 1 let µ = 1 let _prefixed = 1 {code}" ), 0, ) .unwrap(); let mut transformer = Transformer::new(); let transformed_statements = transformer.transform(statements).unwrap().replace_spans(); crate::typechecker::TypeChecker::default() .check_statements(transformed_statements) .unwrap() .last() .unwrap() .clone() } fn pretty_print(stmt: &Statement) -> String { let markup = stmt.pretty_print(); (PlainTextFormatter {}).format(&markup, false) } fn equal_pretty(input: &str, expected: &str) { println!(); println!("expected: '{expected}'"); let actual = pretty_print(&parse(input)); println!("actual: '{actual}'"); assert_eq!(actual, expected); } #[test] fn pretty_print_basic() { equal_pretty("2+3", "2 + 3"); equal_pretty("2*3", "2 × 3"); equal_pretty("2^3", "2³"); equal_pretty("2km", "2 kilometer"); equal_pretty("2kilometer", "2 kilometer"); equal_pretty("sin(30°)", "sin(30 degree)"); equal_pretty("2*3*4", "2 × 3 × 4"); equal_pretty("2*(3*4)", "2 × 3 × 4"); equal_pretty("2+3+4", "2 + 3 + 4"); equal_pretty("2+(3+4)", "2 + 3 + 4"); equal_pretty("atan(30cm / 2m)", "atan(30 centimeter / 2 meter)"); equal_pretty("1mrad -> °", "1 milliradian ➞ degree"); equal_pretty("2km+2cm -> in", "2 kilometer + 2 centimeter ➞ inch"); equal_pretty("2^3 + 4^5", "2³ + 4^5"); equal_pretty("2^3 - 4^5", "2³ - 4^5"); equal_pretty("2^3 * 4^5", "2³ × 4^5"); equal_pretty("2 * 3 + 4 * 5", "2 × 3 + 4 × 5"); equal_pretty("2 * 3 / 4", "2 × 3 / 4"); equal_pretty("123.123 km² / s²", "123.123 × kilometer² / second²"); } fn roundtrip_check(code: &str) { println!("Roundtrip check for code = '{code}'"); let ast1 = parse(code); let code_pretty = pretty_print(&ast1); println!(" pretty printed code = '{code_pretty}'"); let ast2 = parse(&code_pretty); assert_eq!(ast1, ast2); } #[test] fn pretty_print_roundtrip_check() { roundtrip_check("1.0"); roundtrip_check("2"); roundtrip_check("1 + 2"); roundtrip_check("-2.3e-12387"); roundtrip_check("2.3e-12387"); roundtrip_check("18379173"); roundtrip_check("2+3"); roundtrip_check("2+3*5"); roundtrip_check("-3^4+2/(4+2*3)"); roundtrip_check("1-2-3-4-(5-6-7)"); roundtrip_check("1/2/3/4/(5/6/7)"); roundtrip_check("kilogram"); roundtrip_check("2meter/second"); roundtrip_check("a+b*c^d-e*f"); roundtrip_check("sin(x)^3"); roundtrip_check("sin(cos(atan(x)+2))^3"); roundtrip_check("2^3^4^5"); roundtrip_check("(2^3)^(4^5)"); roundtrip_check("sqrt(1.4^2 + 1.5^2) * cos(pi/3)^2"); roundtrip_check("40 kilometer * 9.8meter/second^2 * 150centimeter"); roundtrip_check("4/3 * pi * r³"); roundtrip_check("vol * density -> kilogram"); roundtrip_check("atan(30 centimeter / 2 meter)"); roundtrip_check("500kilometer/second -> centimeter/second"); roundtrip_check("länge * x_2 * µ * _prefixed"); roundtrip_check("2meter^3"); roundtrip_check("(2meter)^3"); roundtrip_check("-sqrt(-30meter^3)"); roundtrip_check("-3^4"); roundtrip_check("(-3)^4"); roundtrip_check("atan2(2,3)"); roundtrip_check("2^3!"); roundtrip_check("-3!"); roundtrip_check("(-3)!"); roundtrip_check("megapoints"); } #[test] fn pretty_print_dexpr() { roundtrip_check("unit z: Length"); roundtrip_check("unit z: Length * Time"); roundtrip_check("unit z: Length * Time^2"); roundtrip_check("unit z: Length^-3 * Time^2"); roundtrip_check("unit z: Length / Time"); roundtrip_check("unit z: Length / Time^2"); roundtrip_check("unit z: Length / Time^(-2)"); roundtrip_check("unit z: Length / (Time * Mass)"); roundtrip_check("unit z: Length^5 * Time^4 / (Time^2 * Mass^3)"); } } numbat-1.11.0/src/unicode_input.rs000064400000000000000000000035631046102023000152030ustar 00000000000000// We are following Julia here [1], but we only support // a small (useful) subset of sequences for now. // [1] https://docs.julialang.org/en/v1/manual/unicode-input/ pub const UNICODE_INPUT: &[(&[&str], &str)] = &[ // Superscript symbols (&["^-"], "⁻"), (&["pm"], "±"), (&["^1"], "¹"), (&["^2"], "²"), (&["^3"], "³"), (&["^4"], "⁴"), (&["^5"], "⁵"), (&["^6"], "⁶"), (&["^7"], "⁷"), (&["^8"], "⁸"), (&["^9"], "⁹"), // Numbers (&["1/2"], "½"), // Operators (&["cdot"], "⋅"), (&["cdotp"], "·"), (&["times"], "×"), (&["div"], "÷"), (&["to", "rightarrow"], "→"), (&["ge"], "≥"), (&["le"], "≤"), (&["dots", "ldots"], "…"), // Greek alphabet (&["Gamma"], "Γ"), (&["Delta"], "Δ"), (&["Theta"], "Θ"), (&["Lambda"], "Λ"), (&["Pi"], "Π"), (&["Sigma"], "Σ"), (&["Phi"], "Φ"), (&["Psi"], "Ψ"), (&["Omega"], "Ω"), (&["alpha"], "α"), (&["beta"], "β"), (&["gamma"], "γ"), (&["delta"], "δ"), (&["epsilon"], "ε"), (&["varepsilon"], "ϵ"), (&["zeta"], "ζ"), (&["eta"], "η"), (&["theta"], "θ"), (&["vartheta"], "ϑ"), (&["iota"], "ι"), (&["kappa"], "κ"), (&["lambda"], "λ"), (&["mu"], "μ"), (&["nu"], "ν"), (&["xi"], "ξ"), (&["pi"], "π"), (&["rho"], "ρ"), (&["sigma"], "σ"), (&["tau"], "τ"), (&["upsilon"], "υ"), (&["phi"], "ϕ"), (&["varphi"], "φ"), (&["chi"], "χ"), (&["psi"], "ψ"), (&["omega"], "ω"), // Units (&["sterling"], "£"), (&["yen"], "¥"), (&["euro"], "€"), (&["degree"], "°"), (&["ohm"], "Ω"), (&["Angstrom"], "Å"), (&["percent"], "%"), (&["perthousand"], "‰"), (&["pertenthousand"], "‱"), // Constants (&["hbar"], "ℏ"), (&["planck"], "ℎ"), ]; numbat-1.11.0/src/unit.rs000064400000000000000000000640621046102023000133160ustar 00000000000000use std::{fmt::Display, ops::Div}; use itertools::Itertools; use num_traits::{ToPrimitive, Zero}; use crate::{ arithmetic::{pretty_exponent, Exponent, Power, Rational}, number::Number, prefix::Prefix, prefix_parser::AcceptsPrefix, product::{Canonicalize, Product}, }; pub type ConversionFactor = Number; /// A unit can either be a base/fundamental unit or it is derived from another unit. /// In the latter case, a conversion factor to the defining unit has to be specified. #[derive(Debug, Clone, PartialEq, Eq)] pub enum UnitKind { Base, Derived(ConversionFactor, Unit), } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CanonicalName { pub name: String, pub accepts_prefix: AcceptsPrefix, } impl CanonicalName { pub fn new(name: &str, accepts_prefix: AcceptsPrefix) -> Self { Self { name: name.into(), accepts_prefix, } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnitIdentifier { pub name: String, pub canonical_name: CanonicalName, kind: UnitKind, } #[derive(Clone, Debug)] pub struct BaseUnitAndFactor(pub Unit, pub Number); impl std::iter::Product for BaseUnitAndFactor { fn product>(iter: I) -> Self { let (fst, snd) = iter.tee(); BaseUnitAndFactor(fst.map(|i| i.0).product(), snd.map(|i| i.1).product()) } } impl UnitIdentifier { pub fn is_base(&self) -> bool { matches!(self.kind, UnitKind::Base) } pub fn unit_and_factor(&self) -> BaseUnitAndFactor { match &self.kind { UnitKind::Base => BaseUnitAndFactor( Unit::new_base(&self.name, self.canonical_name.clone()), Number::from_f64(1.0), ), UnitKind::Derived(factor, defining_unit) => { BaseUnitAndFactor(defining_unit.clone(), *factor) } } } pub fn base_unit_and_factor(&self) -> BaseUnitAndFactor { match &self.kind { UnitKind::Base => BaseUnitAndFactor( Unit::new_base(&self.name, self.canonical_name.clone()), Number::from_f64(1.0), ), UnitKind::Derived(factor, defining_unit) => { let BaseUnitAndFactor(base_unit, defining_unit_factor) = defining_unit .iter() .map( |UnitFactor { unit_id, prefix, exponent, }| { let BaseUnitAndFactor(base_unit, base_unit_factor) = unit_id.base_unit_and_factor(); BaseUnitAndFactor( base_unit.power(*exponent), (prefix.factor() * base_unit_factor) .pow(&Number::from_f64(exponent.to_f64().unwrap())), ) }, ) .product(); BaseUnitAndFactor(base_unit, *factor * defining_unit_factor) } } } pub fn sort_key(&self) -> Vec<(String, Exponent)> { use num_integer::Integer; // TODO: this is more or less a hack. instead of properly sorting by physical // dimension, we sort by the name of the corresponding base unit(s). match &self.kind { UnitKind::Base => vec![(self.name.clone(), Exponent::from_integer(1))], UnitKind::Derived(_, defining_unit) => { let base_unit = defining_unit.to_base_unit_representation().0; let mut key: Vec<_> = base_unit .canonicalized() .iter() .flat_map(|f| { let mut k = f.unit_id.sort_key(); debug_assert!(k.len() == 1); k[0].1 = f.exponent; k }) .collect(); if !key.is_empty() { // Normalize the sign of the exponents. This is useful to consider // 's' and 'Hz' for merging. if key[0].1 < 0.into() { key.iter_mut().for_each(|p| p.1 = -p.1); } // Multiply by the product of all divisors to make all exponents // integers. This is needed for the next step. let factor: i128 = key.iter().map(|p| p.1.numer()).product(); key.iter_mut().for_each(|p| p.1 *= factor); // Now divide every factor by the greatest common divisor. This is // useful to consider g·m² and g²·m⁴ for merging (but not g·m² and g·m³). debug_assert!(key[0].1.is_integer()); let mut common_divisor: i128 = key[0].1.to_integer(); for p in &key[1..] { debug_assert!(p.1.is_integer()); common_divisor = common_divisor.gcd(&p.1.to_integer()); } key.iter_mut().for_each(|p| p.1 /= common_divisor); } key } } } } impl PartialOrd for UnitIdentifier { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for UnitIdentifier { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.sort_key().cmp(&other.sort_key()) } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct UnitFactor { pub unit_id: UnitIdentifier, pub prefix: Prefix, pub exponent: Exponent, } impl Canonicalize for UnitFactor { type MergeKey = (Prefix, UnitIdentifier); fn merge_key(&self) -> Self::MergeKey { (self.prefix, self.unit_id.clone()) } fn merge(self, other: Self) -> Self { UnitFactor { prefix: self.prefix, unit_id: self.unit_id, exponent: self.exponent + other.exponent, } } fn is_trivial(&self) -> bool { self.exponent == Rational::zero() } } impl Power for UnitFactor { fn power(self, e: Exponent) -> Self { UnitFactor { prefix: self.prefix, unit_id: self.unit_id, exponent: self.exponent * e, } } } impl Display for UnitFactor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let prefix = if self.unit_id.canonical_name.accepts_prefix.short { self.prefix.as_string_short() } else { self.prefix.as_string_long() }; write!( f, "{}{}{}", prefix, self.unit_id.canonical_name.name, pretty_exponent(&self.exponent) ) } } pub type Unit = Product; impl Unit { pub fn scalar() -> Self { Self::unity() } pub fn is_scalar(&self) -> bool { self == &Self::scalar() } pub fn new_base(name: &str, canonical_name: CanonicalName) -> Self { Unit::from_factor(UnitFactor { prefix: Prefix::none(), unit_id: UnitIdentifier { name: name.into(), canonical_name, kind: UnitKind::Base, }, exponent: Rational::from_integer(1), }) } pub fn new_derived( name: &str, canonical_name: CanonicalName, factor: ConversionFactor, base_unit: Unit, ) -> Self { Unit::from_factor(UnitFactor { prefix: Prefix::none(), unit_id: UnitIdentifier { name: name.into(), canonical_name, kind: UnitKind::Derived(factor, base_unit), }, exponent: Rational::from_integer(1), }) } pub fn with_prefix(self, prefix: Prefix) -> Self { let mut factors: Vec<_> = self.into_iter().collect(); debug_assert!(!factors.is_empty()); debug_assert!(factors[0].prefix == Prefix::none()); factors[0].prefix = prefix; Self::from_factors(factors) } pub fn to_base_unit_representation(&self) -> (Self, ConversionFactor) { // TODO: reduce wrapping/unwrapping and duplication. let base_unit_representation = self .iter() .map( |UnitFactor { prefix: _, unit_id: base_unit, exponent, }| { base_unit.base_unit_and_factor().0.power(*exponent) }, ) .product::() .canonicalized(); let factor = self .iter() .map( |UnitFactor { prefix, unit_id: base_unit, exponent, }| { (prefix.factor() * base_unit.base_unit_and_factor().1) .pow(&Number::from_f64(exponent.to_f64().unwrap())) // TODO do we want to use exponent.to_f64? }, ) .product(); (base_unit_representation, factor) } #[cfg(test)] pub fn meter() -> Self { Self::new_base( "meter", CanonicalName::new("m", AcceptsPrefix::only_short()), ) } #[cfg(test)] pub fn centimeter() -> Self { Self::new_base( "meter", CanonicalName::new("m", AcceptsPrefix::only_short()), ) .with_prefix(Prefix::centi()) } #[cfg(test)] pub fn millimeter() -> Self { Self::new_base( "meter", CanonicalName::new("m", AcceptsPrefix::only_short()), ) .with_prefix(Prefix::milli()) } #[cfg(test)] pub fn kilometer() -> Self { Self::new_base( "meter", CanonicalName::new("m", AcceptsPrefix::only_short()), ) .with_prefix(Prefix::kilo()) } #[cfg(test)] pub fn second() -> Self { Self::new_base( "second", CanonicalName::new("s", AcceptsPrefix::only_short()), ) } #[cfg(test)] pub fn gram() -> Self { Self::new_base("gram", CanonicalName::new("g", AcceptsPrefix::only_short())) } #[cfg(test)] pub fn kilogram() -> Self { Self::gram().with_prefix(Prefix::kilo()) } #[cfg(test)] pub fn kelvin() -> Self { Self::new_base( "kelvin", CanonicalName::new("K", AcceptsPrefix::only_short()), ) } #[cfg(test)] pub fn radian() -> Self { Self::new_derived( "radian", CanonicalName::new("rad", AcceptsPrefix::only_long()), Number::from_f64(1.0), Self::meter() / Self::meter(), ) } #[cfg(test)] pub fn degree() -> Self { Self::new_derived( "degree", CanonicalName::new("°", AcceptsPrefix::none()), Number::from_f64(std::f64::consts::PI / 180.0), Self::radian(), ) } #[cfg(test)] pub fn percent() -> Self { Self::new_derived( "percent", CanonicalName::new("%", AcceptsPrefix::none()), Number::from_f64(1e-2), Self::scalar(), ) } #[cfg(test)] pub fn hertz() -> Self { Self::new_derived( "hertz", CanonicalName::new("Hz", AcceptsPrefix::only_short()), Number::from_f64(1.0), Unit::second().powi(-1), ) } #[cfg(test)] pub fn newton() -> Self { Self::new_derived( "newton", CanonicalName::new("N", AcceptsPrefix::only_short()), Number::from_f64(1.0), Unit::kilogram() * Unit::meter() / Unit::second().powi(2), ) } #[cfg(test)] pub fn minute() -> Self { Self::new_derived( "minute", CanonicalName::new("min", AcceptsPrefix::none()), Number::from_f64(60.0), Self::second(), ) } #[cfg(test)] pub fn hour() -> Self { Self::new_derived( "hour", CanonicalName::new("h", AcceptsPrefix::none()), Number::from_f64(60.0), Self::minute(), ) } #[cfg(test)] pub fn kph() -> Self { Self::new_derived( "kilometer_per_hour", CanonicalName::new("kph", AcceptsPrefix::none()), Number::from_f64(1.0), Self::kilometer() / Self::hour(), ) } #[cfg(test)] pub fn inch() -> Self { Self::new_derived( "inch", CanonicalName::new("in", AcceptsPrefix::none()), Number::from_f64(0.0254), Self::meter(), ) } #[cfg(test)] pub fn gallon() -> Self { Self::new_derived( "gallon", CanonicalName::new("gal", AcceptsPrefix::none()), Number::from_f64(231.0), Self::inch().powi(3), ) } #[cfg(test)] pub fn foot() -> Self { Self::new_derived( "foot", CanonicalName::new("ft", AcceptsPrefix::none()), Number::from_f64(12.0), Self::inch(), ) } #[cfg(test)] pub fn yard() -> Self { Self::new_derived( "yard", CanonicalName::new("yd", AcceptsPrefix::none()), Number::from_f64(3.0), Self::foot(), ) } #[cfg(test)] pub fn mile() -> Self { Self::new_derived( "mile", CanonicalName::new("mi", AcceptsPrefix::none()), Number::from_f64(1760.0), Self::yard(), ) } #[cfg(test)] pub fn bit() -> Self { Self::new_base("bit", CanonicalName::new("bit", AcceptsPrefix::only_long())) } #[cfg(test)] pub fn byte() -> Self { Self::new_derived( "byte", CanonicalName::new("B", AcceptsPrefix::only_short()), Number::from_f64(8.0), Self::bit(), ) } } impl Display for Unit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.as_string(|f| f.exponent, '·', '/', false)) } } /// This function attempts to solves the equation a = C · b^alpha, where /// C is a constant and alpha is a rational exponent. If there is a solution, /// `Some(alpha)` is returned. If not, `None` is returned. /// /// Examples: /// - is_multiple_of(m², m) = Some(2) /// - is_multiple_of(m, m³) = Some(1/3) /// - is_multiple_of(m³, m²) = Some(3/2) /// - is_multiple_of(m²·s², m·s) = Some(2) /// /// - is_multiple_of(m, km) = Some(1) /// - is_multiple_of(m, inch) = Some(1) /// - is_multiple_of(m², inch) = Some(2) /// /// - is_multiple_of(m, s) = None /// - is_multiple_of(m, m·s) = None /// - is_multiple_of(m·s², m²·s²) = None /// pub fn is_multiple_of(a: &Unit, b: &Unit) -> Option { let a_base = a.to_base_unit_representation().0; let b_base = b.to_base_unit_representation().0; if (a_base.clone().div(b_base.clone())) .canonicalized() .is_scalar() { return Some(Exponent::from_integer(1)); } if a_base.is_scalar() { return None; } let a_first = a_base .iter() .next() .expect("At least one factor in non-scalar unit"); if let Some(b_corresponding_factor) = b_base.iter().find(|fb| fb.unit_id == a_first.unit_id) { let alpha = a_first.exponent / b_corresponding_factor.exponent; // Make sure that this is also correct for all other factors: if a_base.div(b_base.power(alpha)).is_scalar() { Some(alpha) } else { None } } else { None } } #[cfg(test)] mod tests { use approx::assert_relative_eq; use super::*; #[test] fn division() { let meter_per_second = Unit::from_factors([ UnitFactor { prefix: Prefix::none(), unit_id: UnitIdentifier { name: "meter".into(), canonical_name: CanonicalName::new("m", AcceptsPrefix::only_short()), kind: UnitKind::Base, }, exponent: Rational::from_integer(1), }, UnitFactor { prefix: Prefix::none(), unit_id: UnitIdentifier { name: "second".into(), canonical_name: CanonicalName::new("s", AcceptsPrefix::only_short()), kind: UnitKind::Base, }, exponent: Rational::from_integer(-1), }, ]); assert_eq!(Unit::meter() / Unit::second(), meter_per_second); } #[test] fn canonicalization() { let assert_same_representation = |lhs: Unit, rhs: Unit| { // we collect the unit factors into a vector here instead of directly comaring the units. // Otherwise the tests would always succeed because the PartialEq implementation on units // performs canonicalization. assert_eq!( lhs.into_iter().collect::>(), rhs.into_iter().collect::>() ); }; { let unit = Unit::meter() * Unit::second() * Unit::meter() * Unit::second().powi(2); assert_same_representation( unit.canonicalized(), Unit::meter().powi(2) * Unit::second().powi(3), ); } { let unit = Unit::meter() * Unit::second() * Unit::meter() * Unit::hertz(); assert_same_representation( unit.canonicalized(), Unit::meter().powi(2) * Unit::second() * Unit::hertz(), ); } { let unit = Unit::meter() * Unit::second() * Unit::millimeter(); assert_same_representation( unit.canonicalized(), Unit::millimeter() * Unit::meter() * Unit::second(), ); } { let unit = Unit::meter() * Unit::second() * Unit::meter() * Unit::second().powi(-1); assert_same_representation(unit.canonicalized(), Unit::meter().powi(2)); } { let unit = Unit::meter().powi(-1) * Unit::second() * Unit::meter() * Unit::second().powi(-1); assert_same_representation(unit.canonicalized(), Unit::scalar()); } } #[test] fn with_prefix() { let millimeter = Unit::meter().with_prefix(Prefix::milli()); assert_eq!( millimeter, Unit::from_factors([UnitFactor { prefix: Prefix::Metric(-3), unit_id: UnitIdentifier { name: "meter".into(), canonical_name: CanonicalName::new("m", AcceptsPrefix::only_short()), kind: UnitKind::Base, }, exponent: Rational::from_integer(1), }]) ); } #[test] fn to_base_unit_representation_basic() { let hour = Unit::hour(); let (base_unit_representation, conversion_factor) = hour.to_base_unit_representation(); assert_eq!(base_unit_representation, Unit::second()); assert_relative_eq!(conversion_factor.to_f64(), 3600.0, epsilon = 1e-6); } #[test] fn to_base_unit_representation_percent() { let percent = Unit::percent(); let (base_unit_representation, conversion_factor) = percent.to_base_unit_representation(); assert_eq!(base_unit_representation, Unit::scalar()); assert_eq!(conversion_factor.to_f64(), 0.01); } #[test] fn to_base_unit_representation_combined() { let mile_per_hour = Unit::mile() / Unit::hour(); let (base_unit_representation, conversion_factor) = mile_per_hour.to_base_unit_representation(); assert_eq!(base_unit_representation, Unit::meter() / Unit::second()); assert_relative_eq!( conversion_factor.to_f64(), 1609.344 / 3600.0, epsilon = 1e-6 ); } #[test] fn to_string() { assert_eq!(Unit::meter().to_string(), "m"); assert_eq!(Unit::meter().powi(2).to_string(), "m²"); assert_eq!(Unit::meter().powi(3).to_string(), "m³"); assert_eq!(Unit::meter().powi(4).to_string(), "m⁴"); assert_eq!(Unit::meter().powi(8).to_string(), "m^8"); assert_eq!(Unit::meter().powi(-1).to_string(), "m⁻¹"); assert_eq!(Unit::meter().powi(-4).to_string(), "m⁻⁴"); assert_eq!(Unit::meter().powi(-8).to_string(), "m^(-8)"); assert_eq!( (Unit::meter() * Unit::meter() * Unit::second()) .canonicalized() .to_string(), "m²·s" ); assert_eq!( (Unit::meter() * Unit::second() * Unit::second()) .canonicalized() .to_string(), "m·s²" ); assert_eq!( (Unit::meter() / Unit::second()).canonicalized().to_string(), "m/s" ); assert_eq!( (Unit::meter() / (Unit::second() * Unit::second())) .canonicalized() .to_string(), "m/s²" ); assert_eq!( (Unit::kilometer() * Unit::second() * Unit::second()) .canonicalized() .to_string(), "km·s²" ); assert_eq!( (Unit::meter() / (Unit::second() * Unit::second() * Unit::kilogram())) .canonicalized() .to_string(), "m/(kg·s²)" ); assert_eq!( (Unit::meter() * Unit::second().with_prefix(Prefix::milli()) * Unit::second()) .canonicalized() .to_string(), "m·ms·s" ); assert_eq!(Unit::meter().with_prefix(Prefix::micro()).to_string(), "µm"); assert_eq!(Unit::meter().with_prefix(Prefix::milli()).to_string(), "mm"); assert_eq!(Unit::meter().with_prefix(Prefix::centi()).to_string(), "cm"); assert_eq!(Unit::meter().with_prefix(Prefix::deci()).to_string(), "dm"); assert_eq!(Unit::meter().with_prefix(Prefix::hecto()).to_string(), "hm"); assert_eq!(Unit::meter().with_prefix(Prefix::kilo()).to_string(), "km"); assert_eq!(Unit::second().with_prefix(Prefix::mega()).to_string(), "Ms"); assert_eq!(Unit::second().with_prefix(Prefix::giga()).to_string(), "Gs"); assert_eq!(Unit::second().with_prefix(Prefix::tera()).to_string(), "Ts"); assert_eq!( Unit::second() .with_prefix(Prefix::tera()) .powi(2) .to_string(), "Ts²" ); assert_eq!(Unit::byte().with_prefix(Prefix::kibi()).to_string(), "KiB"); assert_eq!(Unit::byte().with_prefix(Prefix::mebi()).to_string(), "MiB"); assert_eq!(Unit::byte().with_prefix(Prefix::gibi()).to_string(), "GiB"); assert_eq!( Unit::byte().with_prefix(Prefix::gibi()).powi(2).to_string(), "GiB²" ); } #[test] fn is_multiple_of_basic() { assert_eq!( is_multiple_of(&Unit::scalar(), &Unit::scalar()), Some(Exponent::from_integer(1)) ); assert_eq!(is_multiple_of(&Unit::scalar(), &Unit::meter()), None); assert_eq!(is_multiple_of(&Unit::meter(), &Unit::scalar()), None); assert_eq!( is_multiple_of(&Unit::meter(), &Unit::meter()), Some(Exponent::new(1, 1)) ); assert_eq!( is_multiple_of(&Unit::meter(), &Unit::meter().powi(3)), Some(Exponent::new(1, 3)) ); assert_eq!( is_multiple_of(&Unit::meter().powi(2), &Unit::meter()), Some(Exponent::new(2, 1)) ); assert_eq!( is_multiple_of(&Unit::meter().powi(2), &Unit::meter().powi(3)), Some(Exponent::new(2, 3)) ); assert_eq!( is_multiple_of(&(Unit::meter() * Unit::second()), &Unit::meter()), None ); assert_eq!( is_multiple_of(&Unit::meter(), &(Unit::meter() * Unit::second())), None ); assert_eq!( is_multiple_of( &(Unit::meter() * Unit::second()), &(Unit::meter() * Unit::second()) ), Some(Exponent::new(1, 1)) ); assert_eq!( is_multiple_of( &(Unit::meter() * Unit::second()), &(Unit::meter() * Unit::second()).powi(2) ), Some(Exponent::new(1, 2)) ); assert_eq!( is_multiple_of( &(Unit::meter() * Unit::second()).powi(3), &(Unit::meter() * Unit::second()).powi(2) ), Some(Exponent::new(3, 2)) ); assert_eq!( is_multiple_of( &(Unit::meter() * Unit::second()), &(Unit::meter().powi(2) * Unit::second().powi(4)) ), None ); assert_eq!( is_multiple_of( &(Unit::meter() * Unit::second().powi(2)), &(Unit::meter().powi(2) * Unit::second().powi(4)) ), Some(Exponent::new(1, 2)) ); } #[test] fn is_multiple_of_with_factor() { assert_eq!( is_multiple_of(&Unit::scalar(), &Unit::radian()), Some(Exponent::new(1, 1)) ); assert_eq!( is_multiple_of(&Unit::radian(), &Unit::scalar()), Some(Exponent::new(1, 1)) ); assert_eq!( is_multiple_of(&Unit::meter(), &Unit::inch()), Some(Exponent::new(1, 1)) ); assert_eq!( is_multiple_of(&Unit::meter(), &Unit::inch().powi(2)), Some(Exponent::new(1, 2)) ); assert_eq!( is_multiple_of(&Unit::meter(), &Unit::kilometer()), Some(Exponent::new(1, 1)) ); assert_eq!( is_multiple_of(&Unit::kilometer(), &Unit::meter()), Some(Exponent::new(1, 1)) ); } } numbat-1.11.0/src/unit_registry.rs000064400000000000000000000034101046102023000152340ustar 00000000000000use crate::markup::Markup; use crate::prefix_parser::AcceptsPrefix; use crate::registry::{BaseRepresentation, BaseRepresentationFactor, Registry, RegistryError}; use crate::typed_ast::Type; use crate::unit::{CanonicalName, Unit}; use thiserror::Error; #[derive(Clone, Error, Debug, PartialEq, Eq)] pub enum UnitRegistryError { #[error("{0}")] RegistryError(RegistryError), } pub type Result = std::result::Result; #[derive(Debug, Clone)] pub struct UnitMetadata { pub type_: Type, pub readable_type: Markup, pub aliases: Vec<(String, AcceptsPrefix)>, pub name: Option, pub canonical_name: CanonicalName, pub url: Option, pub binary_prefixes: bool, pub metric_prefixes: bool, } #[derive(Clone)] pub struct UnitRegistry { pub inner: Registry, } impl UnitRegistry { pub fn new() -> Self { Self { inner: Registry::::default(), } } pub fn add_base_unit(&mut self, name: &str, metadata: UnitMetadata) -> Result<()> { self.inner .add_base_entry(name, metadata) .map_err(UnitRegistryError::RegistryError) } pub fn add_derived_unit( &mut self, name: &str, base_representation: &Unit, metadata: UnitMetadata, ) -> Result<()> { let base_representation_factors = base_representation .iter() .map(|factor| BaseRepresentationFactor(factor.unit_id.name.clone(), factor.exponent)); let base_representation = BaseRepresentation::from_factors(base_representation_factors); self.inner .add_derived_entry(name, base_representation, metadata) .map_err(UnitRegistryError::RegistryError)?; Ok(()) } } numbat-1.11.0/src/value.rs000064400000000000000000000057751046102023000134610ustar 00000000000000use crate::{pretty_print::PrettyPrint, quantity::Quantity}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum FunctionReference { Foreign(String), Normal(String), // TODO: We can get rid of this variant once we implement closures: TzConversion(String), } impl std::fmt::Display for FunctionReference { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { FunctionReference::Foreign(name) => write!(f, ""), FunctionReference::Normal(name) => write!(f, ""), FunctionReference::TzConversion(tz) => { write!(f, "") } } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Value { Quantity(Quantity), Boolean(bool), String(String), /// A DateTime with an associated offset used when pretty printing DateTime(chrono::DateTime), FunctionReference(FunctionReference), } impl Value { #[track_caller] pub fn unsafe_as_quantity(&self) -> &Quantity { if let Value::Quantity(q) = self { q } else { panic!("Expected value to be a quantity"); } } #[track_caller] pub fn unsafe_as_bool(&self) -> bool { if let Value::Boolean(b) = self { *b } else { panic!("Expected value to be a bool"); } } #[track_caller] pub fn unsafe_as_string(&self) -> &str { if let Value::String(s) = self { s } else { panic!("Expected value to be a string"); } } #[track_caller] pub fn unsafe_as_datetime(&self) -> &chrono::DateTime { if let Value::DateTime(dt) = self { dt } else { panic!("Expected value to be a string"); } } #[track_caller] pub fn unsafe_as_function_reference(&self) -> &FunctionReference { if let Value::FunctionReference(inner) = self { inner } else { panic!("Expected value to be a string"); } } } impl std::fmt::Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Value::Quantity(q) => write!(f, "{}", q), Value::Boolean(b) => write!(f, "{}", b), Value::String(s) => write!(f, "\"{}\"", s), Value::DateTime(dt) => write!(f, "datetime(\"{}\")", dt), Value::FunctionReference(r) => write!(f, "{}", r), } } } impl PrettyPrint for Value { fn pretty_print(&self) -> crate::markup::Markup { match self { Value::Quantity(q) => q.pretty_print(), Value::Boolean(b) => b.pretty_print(), Value::String(s) => s.pretty_print(), Value::DateTime(dt) => crate::markup::string(crate::datetime::to_rfc2822_save(dt)), Value::FunctionReference(r) => crate::markup::string(r.to_string()), } } } numbat-1.11.0/src/vm.rs000064400000000000000000001023771046102023000127630ustar 00000000000000use std::{cmp::Ordering, fmt::Display}; use crate::{ ffi::{self, ArityRange, Callable, ForeignFunction}, interpreter::{InterpreterResult, PrintFunction, Result, RuntimeError}, markup::Markup, math, number::Number, prefix::Prefix, quantity::{Quantity, QuantityError}, unit::Unit, unit_registry::{UnitMetadata, UnitRegistry}, value::{FunctionReference, Value}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum Op { /// Push the value of the specified constant onto the stack LoadConstant, /// Add a prefix to the unit on the stack ApplyPrefix, /// This is a special operation for declaring derived units. /// It takes two operands: a global identifier index and a /// constant index. /// It pops the current quantity from the stack and creates /// a new derived unit whose name is specified by the global /// identifier index. It then proceeds to assign a value of /// `1 ` to the constant with the given index. SetUnitConstant, /// Push the value of the specified local variable onto the stack (even /// though it is already on the stack, somewhere lower down). GetLocal, /// Similar to GetLocal, but get variable from surrounding scope GetUpvalue, /// Get the last stored result (_ and ans) GetLastResult, /// Negate the top of the stack Negate, /// Evaluate the factorial of the top of the stack Factorial, /// Pop two values off the stack, add them, push the result onto the stack. Add, /// Similar to Add. Subtract, /// Similar to Add. Multiply, /// Similar to Add. Divide, /// Similar to Add. Power, /// Similar to Add. ConvertTo, /// Similar to Add: LessThan, GreaterThan, LessOrEqual, GreatorOrEqual, Equal, NotEqual, LogicalAnd, LogicalOr, LogicalNeg, /// Similar to Add, but has DateTime on the LHS and a quantity on the RHS AddToDateTime, /// Similar to Sub, but has DateTime on the LHS and a quantity on the RHS SubFromDateTime, /// Computes the difference between two DateTimes DiffDateTime, /// Move IP forward by the given offset argument if the popped-of value on /// top of the stack is false. JumpIfFalse, /// Unconditionally move IP forward by the given offset argument Jump, /// Call the specified function with the specified number of arguments Call, /// Same as above, but call a foreign/native function FFICallFunction, /// Same as above, but call a procedure which does not return anything (does not push a value onto the stack) FFICallProcedure, /// Call a callable object CallCallable, /// Print a compile-time string PrintString, /// Combine N strings on the stack into a single part, used by string interpolation JoinString, /// Perform a simplification operation to the current value on the stack FullSimplify, /// Return from the current function Return, } impl Op { fn num_operands(self) -> usize { match self { Op::SetUnitConstant | Op::Call | Op::FFICallFunction | Op::FFICallProcedure => 2, Op::LoadConstant | Op::ApplyPrefix | Op::GetLocal | Op::GetUpvalue | Op::PrintString | Op::JoinString | Op::JumpIfFalse | Op::Jump | Op::CallCallable => 1, Op::Negate | Op::Factorial | Op::Add | Op::AddToDateTime | Op::Subtract | Op::SubFromDateTime | Op::DiffDateTime | Op::Multiply | Op::Divide | Op::Power | Op::ConvertTo | Op::LessThan | Op::GreaterThan | Op::LessOrEqual | Op::GreatorOrEqual | Op::Equal | Op::NotEqual | Op::LogicalAnd | Op::LogicalOr | Op::LogicalNeg | Op::FullSimplify | Op::Return | Op::GetLastResult => 0, } } fn to_string(self) -> &'static str { match self { Op::LoadConstant => "LoadConstant", Op::ApplyPrefix => "ApplyPrefix", Op::SetUnitConstant => "SetUnitConstant", Op::GetLocal => "GetLocal", Op::GetUpvalue => "GetUpvalue", Op::GetLastResult => "GetLastResult", Op::Negate => "Negate", Op::Factorial => "Factorial", Op::Add => "Add", Op::AddToDateTime => "AddDateTime", Op::Subtract => "Subtract", Op::SubFromDateTime => "SubDateTime", Op::DiffDateTime => "DiffDateTime", Op::Multiply => "Multiply", Op::Divide => "Divide", Op::Power => "Power", Op::ConvertTo => "ConvertTo", Op::LessThan => "LessThan", Op::GreaterThan => "GreaterThan", Op::LessOrEqual => "LessOrEqual", Op::GreatorOrEqual => "GreatorOrEqual", Op::Equal => "Equal", Op::NotEqual => "NotEqual", Op::LogicalAnd => "LogicalAnd", Op::LogicalOr => "LogicalOr", Op::LogicalNeg => "LogicalNeg", Op::JumpIfFalse => "JumpIfFalse", Op::Jump => "Jump", Op::Call => "Call", Op::FFICallFunction => "FFICallFunction", Op::FFICallProcedure => "FFICallProcedure", Op::CallCallable => "CallCallable", Op::PrintString => "PrintString", Op::JoinString => "JoinString", Op::FullSimplify => "FullSimplify", Op::Return => "Return", } } } #[derive(Clone, Debug)] pub enum Constant { Scalar(f64), Unit(Unit), Boolean(bool), String(String), FunctionReference(FunctionReference), } impl Constant { fn to_value(&self) -> Value { match self { Constant::Scalar(n) => Value::Quantity(Quantity::from_scalar(*n)), Constant::Unit(u) => Value::Quantity(Quantity::from_unit(u.clone())), Constant::Boolean(b) => Value::Boolean(*b), Constant::String(s) => Value::String(s.clone()), Constant::FunctionReference(inner) => Value::FunctionReference(inner.clone()), } } } impl Display for Constant { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Constant::Scalar(n) => write!(f, "{}", n), Constant::Unit(unit) => write!(f, "{}", unit), Constant::Boolean(val) => write!(f, "{}", val), Constant::String(val) => write!(f, "\"{}\"", val), Constant::FunctionReference(inner) => write!(f, "{}", inner), } } } #[derive(Clone)] struct CallFrame { /// The function being executed, index into [Vm]s `bytecode` vector. function_idx: usize, /// Instruction "pointer". An index into the bytecode of the currently /// executed function. ip: usize, /// Frame "pointer". Where on the stack do arguments and local variables /// start? fp: usize, } impl CallFrame { fn root() -> Self { CallFrame { function_idx: 0, ip: 0, fp: 0, } } } pub struct ExecutionContext<'a> { pub print_fn: &'a mut PrintFunction, } #[derive(Clone)] pub struct Vm { /// The actual code of the program, structured by function name. The code /// for the global scope is at index 0 under the function name `
`. bytecode: Vec<(String, Vec)>, /// An index into the `bytecode` vector referring to the function which is /// currently being compiled. current_chunk_index: usize, /// Constants are numbers like '1.4' or a [Unit] like 'meter'. pub constants: Vec, /// Unit prefixes in use prefixes: Vec, /// Strings/text that is already available at compile time strings: Vec, /// Meta information about derived units: /// - Unit name /// - Canonical name /// - Metadata unit_information: Vec<(String, Option, UnitMetadata)>, /// Result of the last expression last_result: Option, /// List of registered native/foreign functions ffi_callables: Vec<&'static ForeignFunction>, /// The call stack frames: Vec, /// The stack of the VM. stack: Vec, /// Whether or not to run in debug mode. debug: bool, pub unit_registry: UnitRegistry, } impl Vm { pub fn new() -> Self { Self { bytecode: vec![("
".into(), vec![])], current_chunk_index: 0, constants: vec![], prefixes: vec![], strings: vec![], unit_information: vec![], last_result: None, ffi_callables: ffi::procedures().iter().map(|(_, ff)| ff).collect(), frames: vec![CallFrame::root()], stack: vec![], debug: false, unit_registry: UnitRegistry::new(), } } pub fn set_debug(&mut self, activate: bool) { self.debug = activate; } // The following functions are helpers for the compilation process fn current_chunk_mut(&mut self) -> &mut Vec { &mut self.bytecode[self.current_chunk_index].1 } fn push_u16(chunk: &mut Vec, data: u16) { let arg_bytes = data.to_le_bytes(); chunk.push(arg_bytes[0]); chunk.push(arg_bytes[1]); } pub fn add_op(&mut self, op: Op) { self.current_chunk_mut().push(op as u8); } pub fn add_op1(&mut self, op: Op, arg: u16) { let current_chunk = self.current_chunk_mut(); current_chunk.push(op as u8); Self::push_u16(current_chunk, arg) } pub(crate) fn add_op2(&mut self, op: Op, arg1: u16, arg2: u16) { let current_chunk = self.current_chunk_mut(); current_chunk.push(op as u8); Self::push_u16(current_chunk, arg1); Self::push_u16(current_chunk, arg2); } pub fn current_offset(&self) -> u16 { self.bytecode[self.current_chunk_index].1.len() as u16 } pub fn patch_u16_value_at(&mut self, offset: u16, arg: u16) { let offset = offset as usize; let chunk = self.current_chunk_mut(); chunk[offset] = (arg & 0xff) as u8; chunk[offset + 1] = ((arg >> 8) & 0xff) as u8; } pub fn add_constant(&mut self, constant: Constant) -> u16 { self.constants.push(constant); assert!(self.constants.len() <= u16::MAX as usize); (self.constants.len() - 1) as u16 // TODO: this can overflow, see above } pub fn add_prefix(&mut self, prefix: Prefix) -> u16 { if let Some(idx) = self.prefixes.iter().position(|p| p == &prefix) { idx as u16 } else { self.prefixes.push(prefix); assert!(self.constants.len() <= u16::MAX as usize); (self.prefixes.len() - 1) as u16 // TODO: this can overflow, see above } } pub fn add_unit_information( &mut self, unit_name: &str, canonical_unit_name: Option<&str>, metadata: UnitMetadata, ) -> u16 { if let Some(idx) = self.unit_information.iter().position(|i| i.0 == unit_name) { return idx as u16; } self.unit_information.push(( unit_name.to_owned(), canonical_unit_name.map(|s| s.to_owned()), metadata, )); assert!(self.unit_information.len() <= u16::MAX as usize); (self.unit_information.len() - 1) as u16 // TODO: this can overflow, see above } pub(crate) fn begin_function(&mut self, name: &str) { self.bytecode.push((name.into(), vec![])); self.current_chunk_index = self.bytecode.len() - 1 } pub(crate) fn end_function(&mut self) { // Continue compilation of "main"/global code self.current_chunk_index = 0; } pub(crate) fn get_function_idx(&self, name: &str) -> u16 { // We search backwards to allow for functions // to be overwritten. let rev_position = self .bytecode .iter() .rev() .position(|(n, _)| n == name) .unwrap(); let position = self.bytecode.len() - 1 - rev_position; assert!(position <= u16::MAX as usize); position as u16 } pub(crate) fn add_foreign_function(&mut self, name: &str, arity: ArityRange) { let ff = ffi::functions().get(name).unwrap(); assert!(ff.arity == arity); self.ffi_callables.push(ff); } pub(crate) fn get_ffi_callable_idx(&self, name: &str) -> Option { // TODO: this is a linear search that can certainly be optimized let position = self.ffi_callables.iter().position(|ff| ff.name == name)?; assert!(position <= u16::MAX as usize); Some(position as u16) } pub fn disassemble(&self) { if !self.debug { return; } eprintln!(); eprintln!(".CONSTANTS"); for (idx, constant) in self.constants.iter().enumerate() { eprintln!(" {:04} {}", idx, constant); } eprintln!(".IDENTIFIERS"); for (idx, identifier) in self.unit_information.iter().enumerate() { eprintln!(" {:04} {}", idx, identifier.0); } for (idx, (function_name, bytecode)) in self.bytecode.iter().enumerate() { eprintln!(".CODE {idx} ({name})", idx = idx, name = function_name); let mut offset = 0; while offset < bytecode.len() { let this_offset = offset; let op = bytecode[offset]; offset += 1; let op = unsafe { std::mem::transmute::(op) }; let mut operands: Vec = vec![]; for _ in 0..op.num_operands() { let operand = u16::from_le_bytes(bytecode[offset..(offset + 2)].try_into().unwrap()); operands.push(operand); offset += 2; } let operands_str = operands .iter() .map(u16::to_string) .collect::>() .join(" "); eprint!( " {:04} {:<13} {}", this_offset, op.to_string(), operands_str, ); if op == Op::LoadConstant { eprint!(" (value: {})", self.constants[operands[0] as usize]); } else if op == Op::Call { eprint!( " ({}, num_args={})", self.bytecode[operands[0] as usize].0, operands[1] as usize ); } eprintln!(); } } eprintln!(); } // The following functions are helpers for the actual execution of the code fn current_frame(&self) -> &CallFrame { self.frames.last().expect("Call stack is not empty") } fn current_frame_mut(&mut self) -> &mut CallFrame { self.frames.last_mut().expect("Call stack is not empty") } fn read_byte(&mut self) -> u8 { let frame = self.current_frame(); let byte = self.bytecode[frame.function_idx].1[frame.ip]; self.current_frame_mut().ip += 1; byte } fn read_u16(&mut self) -> u16 { let bytes = [self.read_byte(), self.read_byte()]; u16::from_le_bytes(bytes) } fn push_quantity(&mut self, quantity: Quantity) { self.stack.push(Value::Quantity(quantity)); } fn push_bool(&mut self, boolean: bool) { self.stack.push(Value::Boolean(boolean)); } fn push(&mut self, value: Value) { self.stack.push(value); } #[track_caller] fn pop_quantity(&mut self) -> Quantity { match self.pop() { Value::Quantity(q) => q, _ => panic!("Expected quantity to be on the top of the stack"), } } #[track_caller] fn pop_bool(&mut self) -> bool { self.pop().unsafe_as_bool() } #[track_caller] fn pop_datetime(&mut self) -> chrono::DateTime { match self.pop() { Value::DateTime(q) => q, _ => panic!("Expected datetime to be on the top of the stack"), } } #[track_caller] fn pop(&mut self) -> Value { self.stack.pop().expect("stack should not be empty") } pub fn run(&mut self, ctx: &mut ExecutionContext) -> Result { let old_stack = self.stack.clone(); let result = self.run_without_cleanup(ctx); if result.is_err() { // Perform cleanup: clear the stack and move IP to the end. // This is useful for the REPL. // // TODO(minor): is this really enough? Shouldn't we also remove // the bytecode? self.stack = old_stack; // Reset the call stack // TODO: move the following to a function? self.frames.clear(); self.frames.push(CallFrame::root()); self.frames[0].ip = self.bytecode[0].1.len(); } result } fn is_at_the_end(&self) -> bool { self.current_frame().ip >= self.bytecode[self.current_frame().function_idx].1.len() } fn run_without_cleanup(&mut self, ctx: &mut ExecutionContext) -> Result { let mut result_last_statement = None; while !self.is_at_the_end() { self.debug(); let op = unsafe { std::mem::transmute::(self.read_byte()) }; match op { Op::LoadConstant => { let constant_idx = self.read_u16(); self.stack .push(self.constants[constant_idx as usize].to_value()); } Op::ApplyPrefix => { let quantity = self.pop_quantity(); let prefix_idx = self.read_u16(); let prefix = self.prefixes[prefix_idx as usize]; self.push_quantity(Quantity::new( *quantity.unsafe_value(), quantity.unit().clone().with_prefix(prefix), )); } Op::SetUnitConstant => { let unit_information_idx = self.read_u16(); let constant_idx = self.read_u16(); let conversion_value = self.pop_quantity(); let unit_information = &self.unit_information[unit_information_idx as usize]; let defining_unit = conversion_value.unit(); let (base_unit_representation, _) = defining_unit.to_base_unit_representation(); self.unit_registry .add_derived_unit( &unit_information.0, &base_unit_representation, unit_information.2.clone(), ) .map_err(RuntimeError::UnitRegistryError)?; self.constants[constant_idx as usize] = Constant::Unit(Unit::new_derived( &unit_information.0, unit_information.2.canonical_name.clone(), *conversion_value.unsafe_value(), defining_unit.clone(), )); } Op::GetLocal => { let slot_idx = self.read_u16() as usize; let stack_idx = self.current_frame().fp + slot_idx; self.push(self.stack[stack_idx].clone()); } Op::GetUpvalue => { let stack_idx = self.read_u16() as usize; self.push(self.stack[stack_idx].clone()); } Op::GetLastResult => { self.push(self.last_result.as_ref().unwrap().clone()); } op @ (Op::Add | Op::Subtract | Op::Multiply | Op::Divide | Op::Power | Op::ConvertTo) => { let rhs = self.pop_quantity(); let lhs = self.pop_quantity(); let result = match op { Op::Add => &lhs + &rhs, Op::Subtract => &lhs - &rhs, Op::Multiply => Ok(lhs * rhs), Op::Divide => { Ok(lhs.checked_div(rhs).ok_or(RuntimeError::DivisionByZero)?) } Op::Power => lhs.power(rhs), Op::ConvertTo => lhs.convert_to(rhs.unit()), _ => unreachable!(), }; self.push_quantity(result.map_err(RuntimeError::QuantityError)?); } op @ (Op::AddToDateTime | Op::SubFromDateTime) => { let rhs = self.pop_quantity(); let lhs = self.pop_datetime(); // for time, the base unit is in seconds let base = rhs.to_base_unit_representation(); let seconds_f = base.unsafe_value().to_f64(); let duration = chrono::Duration::try_seconds(seconds_f.trunc() as i64) .ok_or(RuntimeError::DurationOutOfRange)? + chrono::Duration::nanoseconds( (seconds_f.fract() * 1_000_000_000f64).round() as i64, ); self.push(Value::DateTime(match op { Op::AddToDateTime => lhs .checked_add_signed(duration) .ok_or(RuntimeError::DateTimeOutOfRange)?, Op::SubFromDateTime => lhs .checked_sub_signed(duration) .ok_or(RuntimeError::DateTimeOutOfRange)?, _ => unreachable!(), })); } Op::DiffDateTime => { let unit = self.pop_quantity(); let rhs = self.pop_datetime(); let lhs = self.pop_datetime(); let duration = lhs - rhs; let duration = duration.subsec_nanos() as f64 / 1_000_000_000f64 + duration.num_seconds() as f64; let ret = Value::Quantity(Quantity::new( Number::from_f64(duration), unit.unit().clone(), )); self.push(ret); } op @ (Op::LessThan | Op::GreaterThan | Op::LessOrEqual | Op::GreatorOrEqual) => { let rhs = self.pop_quantity(); let lhs = self.pop_quantity(); let result = lhs.partial_cmp(&rhs).ok_or_else(|| { RuntimeError::QuantityError(QuantityError::IncompatibleUnits( lhs.unit().clone(), rhs.unit().clone(), )) })?; let result = match op { Op::LessThan => result == Ordering::Less, Op::GreaterThan => result == Ordering::Greater, Op::LessOrEqual => result != Ordering::Greater, Op::GreatorOrEqual => result != Ordering::Less, _ => unreachable!(), }; self.push(Value::Boolean(result)); } op @ (Op::Equal | Op::NotEqual) => { let rhs = self.pop(); let lhs = self.pop(); let result = match op { Op::Equal => lhs == rhs, Op::NotEqual => lhs != rhs, _ => unreachable!(), }; self.push(Value::Boolean(result)); } op @ (Op::LogicalAnd | Op::LogicalOr) => { let rhs = self.pop_bool(); let lhs = self.pop_bool(); let result = match op { Op::LogicalAnd => lhs && rhs, Op::LogicalOr => lhs || rhs, _ => unreachable!(), }; self.push_bool(result); } Op::LogicalNeg => { let rhs = self.pop_bool(); self.push_bool(!rhs); } Op::Negate => { let rhs = self.pop_quantity(); self.push_quantity(-rhs); } Op::Factorial => { let lhs = self .pop_quantity() .as_scalar() .expect("Expected factorial operand to be scalar") .to_f64(); if lhs < 0. { return Err(RuntimeError::FactorialOfNegativeNumber); } else if lhs.fract() != 0. { return Err(RuntimeError::FactorialOfNonInteger); } self.push_quantity(Quantity::from_scalar(math::factorial(lhs))); } Op::JumpIfFalse => { let offset = self.read_u16() as usize; if !self.pop_bool() { self.current_frame_mut().ip += offset; } } Op::Jump => { let offset = self.read_u16() as usize; self.current_frame_mut().ip += offset; } Op::Call => { let function_idx = self.read_u16() as usize; let num_args = self.read_u16() as usize; self.frames.push(CallFrame { function_idx, ip: 0, fp: self.stack.len() - num_args, }) } Op::FFICallFunction | Op::FFICallProcedure => { let function_idx = self.read_u16() as usize; let num_args = self.read_u16() as usize; let foreign_function = &self.ffi_callables[function_idx]; debug_assert!(foreign_function.arity.contains(&num_args)); let mut args = vec![]; for _ in 0..num_args { args.push(self.pop()); } args.reverse(); // TODO: use a deque? match &self.ffi_callables[function_idx].callable { Callable::Function(function) => { let result = (function)(&args[..]); self.push(result?); } Callable::Procedure(procedure) => { let result = (procedure)(ctx, &args[..]); match result { std::ops::ControlFlow::Continue(()) => {} std::ops::ControlFlow::Break(runtime_error) => { return Err(runtime_error); } } } } } Op::CallCallable => { let num_args = self.read_u16() as usize; let callable = self.pop(); match callable.unsafe_as_function_reference() { FunctionReference::Normal(name) => { let function_idx = self.get_function_idx(name) as usize; // TODO: unify code with 'Op::Call'? self.frames.push(CallFrame { function_idx, ip: 0, fp: self.stack.len() - num_args, }) } FunctionReference::Foreign(name) => { let function_idx = self .get_ffi_callable_idx(name) .expect("Foreign function exists") as usize; let mut args = vec![]; for _ in 0..num_args { args.push(self.pop()); } args.reverse(); match &self.ffi_callables[function_idx].callable { Callable::Function(function) => { let result = (function)(&args[..]); self.push(result?); } Callable::Procedure(..) => unreachable!("Foreign procedures can not be targeted by a function reference"), } } FunctionReference::TzConversion(tz_name) => { // TODO: implement this using a closure, once we have that in the language let dt = self.pop_datetime(); let tz: chrono_tz::Tz = tz_name .parse() .map_err(|_| RuntimeError::UnknownTimezone(tz_name.into()))?; let dt = dt.with_timezone(&tz).fixed_offset(); self.push(Value::DateTime(dt)); } } } Op::PrintString => { let s_idx = self.read_u16() as usize; let s = &self.strings[s_idx]; self.print(ctx, s); } Op::JoinString => { let num_parts = self.read_u16() as usize; let mut joined = String::new(); for _ in 0..num_parts { let part = match self.pop() { Value::Quantity(q) => q.to_string(), Value::Boolean(b) => b.to_string(), Value::String(s) => s, Value::DateTime(dt) => crate::datetime::to_rfc2822_save(&dt), Value::FunctionReference(r) => r.to_string(), }; joined = part + &joined; // reverse order } self.push(Value::String(joined)) } Op::FullSimplify => match self.pop() { Value::Quantity(q) => { let simplified = q.full_simplify(); self.push_quantity(simplified); } v => self.push(v), }, Op::Return => { if self.frames.len() == 1 { let return_value = self.pop(); self.last_result = Some(return_value.clone()); result_last_statement = Some(return_value); } else { let discarded_frame = self.frames.pop().unwrap(); // Remember the return value which is currently on top of the stack let return_value = self.stack.pop().unwrap(); // Pop off arguments from previous call while self.stack.len() > discarded_frame.fp { self.stack.pop(); } // Push the return value back on top of the stack self.stack.push(return_value); } } } } if let Some(value) = result_last_statement { Ok(InterpreterResult::Value(value)) } else { Ok(InterpreterResult::Continue) } } pub fn debug(&self) { if !self.debug { return; } let frame = self.current_frame(); eprint!( "FRAME = {}, IP = {}, ", self.bytecode[frame.function_idx].0, frame.ip ); eprintln!( "Stack: [{}]", self.stack .iter() .map(|x| x.to_string()) .collect::>() .join("] [") ); } pub fn add_string(&mut self, m: Markup) -> u16 { self.strings.push(m); assert!(self.strings.len() <= u16::MAX as usize); (self.strings.len() - 1) as u16 // TODO: this can overflow, see above } fn print(&self, ctx: &mut ExecutionContext, m: &Markup) { (ctx.print_fn)(m); } } #[test] fn vm_basic() { let mut vm = Vm::new(); vm.add_constant(Constant::Scalar(42.0)); vm.add_constant(Constant::Scalar(1.0)); vm.add_op1(Op::LoadConstant, 0); vm.add_op1(Op::LoadConstant, 1); vm.add_op(Op::Add); vm.add_op(Op::Return); let mut print_fn = |_: &Markup| {}; let mut ctx = ExecutionContext { print_fn: &mut print_fn, }; assert_eq!( vm.run(&mut ctx).unwrap(), InterpreterResult::Value(Value::Quantity(Quantity::from_scalar(42.0 + 1.0))) ); } numbat-1.11.0/tests/common.rs000064400000000000000000000014761046102023000142020ustar 00000000000000use std::path::Path; use numbat::{module_importer::FileSystemImporter, resolver::CodeSource, Context, NumbatError}; use once_cell::sync::Lazy; pub fn get_test_context_without_prelude() -> Context { let module_path = Path::new( &std::env::var_os("CARGO_MANIFEST_DIR") .expect("CARGO_MANIFEST_DIR variable should be set when calling 'cargo test'"), ) .join("modules"); let mut importer = FileSystemImporter::default(); importer.add_path(module_path); Context::new(importer) } pub fn get_test_context() -> Context { static CONTEXT: Lazy> = Lazy::new(|| { let mut context = get_test_context_without_prelude(); let _ = context.interpret("use prelude", CodeSource::Internal)?; Ok(context) }); CONTEXT.clone().unwrap() } numbat-1.11.0/tests/interpreter.rs000064400000000000000000000450231046102023000152510ustar 00000000000000mod common; use common::get_test_context; use insta::assert_snapshot; use numbat::markup::{Formatter, PlainTextFormatter}; use numbat::resolver::CodeSource; use numbat::NumbatError; use numbat::{pretty_print::PrettyPrint, Context, InterpreterResult}; #[track_caller] fn expect_output_with_context(ctx: &mut Context, code: &str, expected_output: impl AsRef) { let expected_output = expected_output.as_ref(); println!("Expecting output '{expected_output}' for code '{code}'"); if let InterpreterResult::Value(val) = ctx.interpret(code, CodeSource::Internal).unwrap().1 { let fmt = PlainTextFormatter {}; let actual_output = fmt.format(&val.pretty_print(), false); assert_eq!(actual_output.trim(), expected_output); } else { panic!(); } } #[track_caller] fn fail(code: &str) -> NumbatError { let mut ctx = get_test_context(); let ret = ctx.interpret(code, CodeSource::Internal); match ret { Err(e) => e, Ok((_stmts, ret)) => { if let InterpreterResult::Value(val) = ret { let fmt = PlainTextFormatter {}; let output = fmt.format(&val.pretty_print(), false); panic!("was supposed to fail but instead got:\n{}", output.trim()) } else { panic!("was supposed to fail but instead got:\n{:?}", ret) } } } } #[track_caller] fn expect_output(code: &str, expected_output: impl AsRef) { let mut ctx = get_test_context(); expect_output_with_context(&mut ctx, code, expected_output) } #[track_caller] fn expect_failure(code: &str, msg_part: &str) { let mut ctx = get_test_context(); if let Err(e) = ctx.interpret(code, CodeSource::Internal) { let error_message = e.to_string(); println!("{}", error_message); assert!(error_message.contains(msg_part)); } else { panic!(); } } #[track_caller] fn get_error_message(code: &str) -> String { let mut ctx = get_test_context(); if let Err(e) = ctx.interpret(code, CodeSource::Internal) { e.to_string() } else { panic!(); } } #[test] fn simple_value() { expect_output("0", "0"); expect_output("0_0", "0"); expect_output("0_0.0_0", "0"); expect_output(".0", "0"); expect_failure("_.0", "Unexpected character in identifier: '.'"); expect_failure("._0", "Expected digit"); expect_output(".0_0", "0"); expect_failure(".0_", "Unexpected character in number literal: '_'"); expect_output("0b0", "0"); expect_output("0b01", "1"); expect_output("0b0_0", "0"); expect_failure("0b012", "Expected base-2 digit"); expect_failure("0b", "Expected base-2 digit"); expect_failure("0b_", "Expected base-2 digit"); expect_failure("0b_0", "Expected base-2 digit"); expect_failure("0b0_", "Expected base-2 digit"); expect_failure("0b0.0", "Expected base-2 digit"); expect_output("0o0", "0"); expect_output("0o01234567", "342_391"); expect_output("0o0_0", "0"); expect_failure("0o012345678", "Expected base-8 digit"); expect_failure("0o", "Expected base-8 digit"); expect_failure("0o_", "Expected base-8 digit"); expect_failure("0o_0", "Expected base-8 digit"); expect_failure("0o0_", "Expected base-8 digit"); expect_failure("0o0.0", "Expected base-8 digit"); expect_output("0x0", "0"); expect_output("0x0123456789abcdef", "8.19855e+16"); expect_output("0x0_0", "0"); expect_failure("0x0123456789abcdefg", "Expected base-16 digit"); expect_failure("0x", "Expected base-16 digit"); expect_failure("0x_", "Expected base-16 digit"); expect_failure("0x_0", "Expected base-16 digit"); expect_failure("0x0_", "Expected base-16 digit"); expect_failure("0x0.0", "Expected base-16 digit"); } #[test] fn test_factorial() { expect_output("0!", "1"); expect_output("4!", "24"); expect_output("4.0!", "24"); expect_output("4 !", "24"); expect_output(" 4 !", "24"); expect_output("(4)!", "24"); expect_output("3!^3", "216"); // Not supported, at least for now. // expect_output("3!³", "216"); expect_output("(3!)^3", "216"); expect_output("3^3!", "729"); expect_output("-5!", "-120"); expect_output("-(5!)", "-120"); expect_output("-(5)!", "-120"); expect_failure( "(-1)!", "Expected factorial argument to be a non-negative integer", ); expect_failure( "1.5!", "Expected factorial argument to be a finite integer number", ); expect_failure( "(-1.5)!", "Expected factorial argument to be a non-negative integer", ); expect_failure( "(2m)!", "Argument of factorial needs to be dimensionless (got Length).", ); } #[test] fn test_exponentiation() { expect_output("3²*2", "18"); expect_output("3² 2", "18"); expect_output("3²·2", "18"); expect_output("3³*2", "54"); expect_output("3³(2)", "54"); expect_output("(1+2)²", "9"); expect_output("2²pi", "12.5664"); expect_output("2² pi", "12.5664"); expect_output("2²·pi", "12.5664"); expect_output("5m² to cm·m", "500 cm·m"); expect_output("2⁵", "32"); expect_output("-4¹", "-4"); expect_output("2⁻¹", "0.5"); expect_output("2⁻²", "0.25"); expect_output("10⁻⁵", "0.00001"); } #[test] fn test_conversions() { expect_output("2in to cm", "5.08 cm"); expect_output("5m^2 -> m*cm", "500 m·cm"); expect_output("5m^2 -> cm*m", "500 cm·m"); expect_output("1 kB / 10 ms -> MB/s", "0.1 MB/s"); expect_output("55! / (6! (55 - 6)!) -> million", "28.9897 million"); } #[test] fn test_implicit_conversion() { let mut ctx = get_test_context(); let _ = ctx.interpret("let x = 5 m", CodeSource::Internal).unwrap(); expect_output_with_context(&mut ctx, "x", "5 m"); expect_output_with_context(&mut ctx, "2x", "10 m"); expect_output_with_context(&mut ctx, "2 x", "10 m"); expect_output_with_context(&mut ctx, "x x", "25 m²"); expect_output_with_context(&mut ctx, "x²", "25 m²"); expect_failure("x2", "Unknown identifier 'x2'"); } #[test] fn test_reset_after_runtime_error() { let mut ctx = get_test_context(); let _ = ctx.interpret("let x = 1", CodeSource::Internal).unwrap(); let res = ctx.interpret("1/0", CodeSource::Internal); assert!(res.is_err()); expect_output_with_context(&mut ctx, "x", "1"); } #[test] fn test_function_inverses() { expect_output("sin(asin(0.1234))", "0.1234"); expect_output("cos(acos(0.1234))", "0.1234"); expect_output("tan(atan(0.1234))", "0.1234"); expect_output("sinh(asinh(0.1234))", "0.1234"); expect_output("cosh(acosh(1.1234))", "1.1234"); expect_output("tanh(atanh(0.1234))", "0.1234"); expect_output("log(exp(0.1234))", "0.1234"); expect_output("log10(10^0.1234)", "0.1234"); expect_output("log2(2^0.1234)", "0.1234"); expect_output("sqr(sqrt(0.1234))", "0.1234"); expect_output("asin(sin(0.1234))", "0.1234"); expect_output("acos(cos(0.1234))", "0.1234"); expect_output("atan(tan(0.1234))", "0.1234"); expect_output("asinh(sinh(0.1234))", "0.1234"); expect_output("acosh(cosh(1.1234))", "1.1234"); expect_output("atanh(tanh(0.1234))", "0.1234"); expect_output("exp(log(0.1234))", "0.1234"); expect_output("10^(log10(0.1234))", "0.1234"); expect_output("2^(log2(0.1234))", "0.1234"); expect_output("sqrt(sqr(0.1234))", "0.1234"); } #[test] fn test_math() { expect_output("sin(90°)", "1"); expect_output("sin(30°)", "0.5"); expect_output("sin(pi/2)", "1"); expect_output("atan2(10, 0) / (pi / 2)", "1"); expect_output("atan2(100 cm, 1 m) / (pi / 4)", "1"); expect_failure( "atan2(100 cm, 1 m²)", "parameter type: Length\n argument type: Length²", ); expect_output("mod(5, 3)", "2"); expect_output("mod(-1, 4)", "3"); expect_output("mod(8 cm, 5 cm)", "3 cm"); expect_output("mod(235 cm, 1 m)", "35 cm"); expect_output("mod(2 m, 7 cm)", "0.04 m"); expect_failure( "mod(8 m, 5 s)", "parameter type: Length\n argument type: Time", ) } #[test] fn test_incompatible_dimension_errors() { assert_snapshot!( get_error_message("kg m / s^2 + kg m^2"), @r###" left hand side: Length × Mass × Time⁻² [= Force] right hand side: Length² × Mass [= MomentOfInertia] "### ); assert_snapshot!( get_error_message("1 + m"), @r###" left hand side: Scalar [= Angle, Scalar, SolidAngle] right hand side: Length Suggested fix: divide the expression on the right hand side by a `Length` factor "### ); assert_snapshot!( get_error_message("m / s + K A"), @r###" left hand side: Length / Time [= Velocity] right hand side: Current × Temperature "### ); assert_snapshot!( get_error_message("m + 1 / m"), @r###" left hand side: Length right hand side: Length⁻¹ [= Wavenumber] Suggested fix: invert the expression on the right hand side "### ); assert_snapshot!( get_error_message("kW -> J"), @r###" left hand side: Length² × Mass × Time⁻³ [= Power] right hand side: Length² × Mass × Time⁻² [= Energy, Torque] Suggested fix: divide the expression on the right hand side by a `Time` factor "### ); assert_snapshot!( get_error_message("sin(1 meter)"), @r###" parameter type: Scalar [= Angle, Scalar, SolidAngle] argument type: Length Suggested fix: divide the function argument by a `Length` factor "### ); assert_snapshot!( get_error_message("let x: Acceleration = 4 m / s"), @r###" specified dimension: Length × Time⁻² [= Acceleration] actual dimension: Length × Time⁻¹ [= Velocity] Suggested fix: divide the right hand side expression by a `Time` factor "### ); assert_snapshot!( get_error_message("unit x: Acceleration = 4 m / s"), @r###" specified dimension: Length × Time⁻² [= Acceleration] actual dimension: Length × Time⁻¹ [= Velocity] Suggested fix: divide the right hand side expression by a `Time` factor "### ); assert_snapshot!( get_error_message("fn acceleration(length: Length, time: Time) -> Acceleration = length / time"), @r###" specified return type: Length × Time⁻² [= Acceleration] actual return type: Length × Time⁻¹ [= Velocity] Suggested fix: divide the expression in the function body by a `Time` factor "### ); } #[test] fn test_temperature_conversions() { expect_output("from_celsius(11.5)", "284.65 K"); expect_output("from_fahrenheit(89.3)", "304.983 K"); expect_output("0 K -> celsius", "-273.15"); expect_output("fahrenheit(30 K)", "-405.67"); expect_output("from_celsius(100) -> celsius", "100"); expect_output("from_fahrenheit(100) -> fahrenheit", "100.0"); expect_output("from_celsius(123 K -> celsius)", "123 K"); expect_output("from_fahrenheit(123 K -> fahrenheit)", "123 K"); expect_output("-40 -> from_fahrenheit -> celsius", "-40"); } #[test] fn test_other_functions() { expect_output("sqrt(4)", "2"); expect_output("log10(100000)", "5"); expect_output("log(e^15)", "15"); expect_output("ln(e^15)", "15"); expect_output("ceil(3.1)", "4"); expect_output("floor(3.9)", "3"); expect_output("round(3.9)", "4"); expect_output("round(3.1)", "3"); } #[test] fn test_last_result_identifier() { let mut ctx = get_test_context(); let _ = ctx.interpret("2 + 3", CodeSource::Internal).unwrap(); expect_output_with_context(&mut ctx, "ans", "5"); let _ = ctx.interpret("1 + 2", CodeSource::Internal).unwrap(); expect_output_with_context(&mut ctx, "_", "3"); } #[test] fn test_misc_examples() { expect_output("1920/16*9", "1080"); expect_output("2^32", "4_294_967_296"); expect_output("sqrt(1.4^2 + 1.5^2) * cos(pi/3)^2", "0.512957"); expect_output("2min + 30s", "2.5 min"); expect_output("2min + 30s -> sec", "150 s"); expect_output("4/3 * pi * (6000km)³", "9.04779e+11 km³"); expect_output("40kg * 9.8m/s^2 * 150cm", "588 kg·m²/s²"); expect_output("sin(30°)", "0.5"); expect_output("60mph -> m/s", "26.8224 m/s"); expect_output("240km/day -> km/h", "10 km/h"); expect_output("1mrad -> °", "0.0572958°"); expect_output("52weeks -> days", "364 day"); expect_output("5in + 2ft -> cm", "73.66 cm"); expect_output("atan(30cm / 2m) -> deg", "8.53077°"); expect_output("6Mbit/s * 1.5h -> GB", "4.05 GB"); expect_output("6Mbit/s * 1.5h -> GiB", "3.77186 GiB"); expect_output("3m/4m", "0.75"); expect_output("4/2*2", "4"); expect_output("1/2 Hz -> s", "0.5 s"); } #[test] fn test_bohr_radius_regression() { // Make sure that the unit is 'm', and not 'F·J²/(C²·kg·m·Hz²)', like we had before expect_output("bohr_radius", "5.29177e-11 m"); } #[test] fn test_full_simplify() { expect_output("5 cm/m", "0.05"); expect_output("hour/second", "3600"); expect_output("5 to cm/m", "500 cm/m"); expect_output( "fn f(x: Scalar) -> Scalar = x to cm/m f(5)", "500 cm/m", ); expect_output("1 Wh/W", "1 Wh/W"); // This output is not great (and should be improved). But we keep this as a regression test for a bug in previous versions. expect_output("1 × (m/s)^2/(m/s)", "1 m/s"); } #[test] fn test_prefixes() { expect_output("hertz second", "1"); expect_output("kilohertz millisecond", "1"); expect_output("megahertz microsecond", "1"); expect_output("gigahertz nanosecond", "1"); expect_output("terahertz picosecond", "1"); expect_output("petahertz femtosecond", "1"); expect_output("exahertz attosecond", "1"); expect_output("zettahertz zeptosecond", "1"); expect_output("yottahertz yoctosecond", "1"); expect_output("ronnahertz rontosecond", "1"); expect_output("quettahertz quectosecond", "1"); } #[test] fn test_parse_errors() { expect_failure( "3kg+", "Expected one of: number, identifier, parenthesized expression", ); expect_failure("let print=2", "Expected identifier after 'let' keyword"); expect_failure( "fn print(x: Scalar) = 1", "Expected identifier after 'fn' keyword", ); } #[test] fn test_name_clash_errors() { expect_failure("let kg=2", "Identifier is already in use: 'kg'"); expect_failure("fn kg(x: Scalar) = 1", "Identifier is already in use: 'kg'"); expect_failure("fn _()=0", "Reserved identifier"); } #[test] fn test_type_check_errors() { expect_failure("foo", "Unknown identifier 'foo'"); expect_failure("let sin=2", "This name is already used by a function"); expect_failure("fn pi() = 1", "This name is already used by a constant"); expect_failure( "fn sin(x)=0", "This name is already used by a foreign function", ); } #[test] fn test_runtime_errors() { expect_failure("1/0", "Division by zero"); } #[test] fn test_comparisons() { expect_output("2 < 3", "true"); expect_output("2 m < 3 m", "true"); expect_output("20 cm < 3 m", "true"); expect_output("2 m < 100 cm", "false"); expect_output("2 > 3", "false"); expect_output("2 m > 3 m", "false"); expect_output("20 cm > 3 m", "false"); expect_output("2 m > 100 cm", "true"); expect_output("2 <= 2", "true"); expect_output("2.1 <= 2", "false"); expect_output("2 >= 2", "true"); expect_output("2 >= 2.1", "false"); expect_output("200 cm == 2 m", "true"); expect_output("201 cm == 2 m", "false"); expect_output("200 cm != 2 m", "false"); expect_output("201 cm != 2 m", "true"); } #[test] fn test_logical() { // negation expect_output("!true", "false"); expect_output("!false", "true"); // or expect_output("true || false", "true"); expect_output("false || false", "false"); // and expect_output("true && false", "false"); expect_output("true && true", "true"); // priority expect_output("false || true && false", "false"); expect_output("false || true && !false", "true"); // Errors insta::assert_display_snapshot!(fail("1 || 2"), @"Expected boolean value"); insta::assert_display_snapshot!(fail("true || 2"), @"Expected boolean value"); insta::assert_display_snapshot!(fail("1 || true"), @"Expected boolean value"); insta::assert_display_snapshot!(fail("1 && 2"), @"Expected boolean value"); insta::assert_display_snapshot!(fail("true && 2"), @"Expected boolean value"); insta::assert_display_snapshot!(fail("1 && true"), @"Expected boolean value"); insta::assert_display_snapshot!(fail("!1"), @"Expected boolean value"); insta::assert_display_snapshot!(fail("!1 || true"), @"Expected boolean value"); } #[test] fn test_conditionals() { expect_output("if 1 < 2 then 3 else 4", "3"); expect_output("if 4 < 3 then 2 else 1", "1"); expect_output( "if 4 > 3 then \"four is larger!\" else \"four is not larger!\"", "four is larger!", ); } #[test] fn test_string_interpolation() { expect_output("\"pi = {pi}!\"", "pi = 3.14159!"); expect_output("\"1 + 2 = {1 + 2}\"", "1 + 2 = 3"); } #[test] fn test_overwrite_regular_function() { expect_output( " fn f(x)=0 fn f(x)=1 f(2)", "1", ); } #[test] fn test_overwrite_inner_function() { expect_output( " fn inner() = 0 fn outer() = inner() fn inner(x) = 1 outer() ", "0", ); } #[test] fn test_override_constants() { expect_output("let x = 1\nlet x = 2\nx", "2"); expect_output("let pi = 4\npi", "4"); } #[test] fn test_overwrite_captured_constant() { expect_output( " let x = 1 fn f() = sin(x) let x = 1 m f() ", "0.841471", ); } #[test] fn test_pretty_print_prefixes() { expect_output("1 megabarn", "1 megabarn"); } #[test] fn test_full_simplify_for_function_calls() { expect_output("floor(1.2 hours / hour)", "1"); } #[test] fn test_datetime_runtime_errors() { expect_failure("datetime(\"2000-01-99\")", "Unrecognized datetime format"); expect_failure("now() -> tz(\"Europe/NonExisting\")", "Unknown timezone"); expect_failure( "date(\"2000-01-01\") + 1e100 years", "Exceeded maximum size for time durations", ); expect_failure( "date(\"2000-01-01\") + 100000000 years", "DateTime out of range", ); expect_failure( "format_datetime(\"%Y-%m-%dT%H%:M\", now())", "Error in datetime format", ) } numbat-1.11.0/tests/prelude_and_examples.rs000064400000000000000000000053611046102023000170670ustar 00000000000000mod common; use common::get_test_context; use numbat::resolver::{CodeSource, ResolverError}; use numbat::{InterpreterResult, NumbatError}; use std::ffi::OsStr; use std::fs; use crate::common::get_test_context_without_prelude; fn assert_runs(code: &str) { let result = get_test_context().interpret(code, CodeSource::Internal); assert!(result.is_ok()); assert!(matches!( result.unwrap().1, InterpreterResult::Value(_) | InterpreterResult::Continue )); } fn assert_runs_without_prelude(code: &str) { let result = get_test_context_without_prelude().interpret(code, CodeSource::Internal); assert!(result.is_ok()); assert!(matches!( result.unwrap().1, InterpreterResult::Value(_) | InterpreterResult::Continue )); } fn assert_parse_error(code: &str) { assert!(matches!( get_test_context().interpret(code, CodeSource::Internal), Err(NumbatError::ResolverError( ResolverError::ParseErrors { .. } )) )); } fn assert_name_resolution_error(code: &str) { assert!(matches!( get_test_context().interpret(code, CodeSource::Internal), Err(NumbatError::NameResolutionError(_)) )); } fn assert_typecheck_error(code: &str) { assert!(matches!( get_test_context().interpret(code, CodeSource::Internal), Err(NumbatError::TypeCheckError(_)) )); } fn assert_runtime_error(code: &str) { assert!(matches!( get_test_context().interpret(code, CodeSource::Internal), Err(NumbatError::RuntimeError(_)) )); } fn run_for_each_file(glob_pattern: &str, f: impl Fn(&str)) { for entry in glob::glob(glob_pattern).unwrap() { let path = entry.unwrap(); if path.extension() != Some(OsStr::new("nbt")) { continue; } println!("Testing example {example:?}", example = path); let example_code = fs::read_to_string(path).unwrap(); f(&example_code); } } #[test] fn modules_are_self_consistent() { run_for_each_file("modules/**/*.nbt", assert_runs_without_prelude); } #[test] fn examples_can_be_parsed_and_interpreted() { run_for_each_file("../examples/*.nbt", assert_runs); } #[test] fn parse_error_examples_fail_as_expected() { run_for_each_file("../examples/parse_error/*.nbt", assert_parse_error); } #[test] fn name_resolution_error_examples_fail_as_expected() { run_for_each_file( "../examples/name_resolution_error/*.nbt", assert_name_resolution_error, ); } #[test] fn typecheck_error_examples_fail_as_expected() { run_for_each_file("../examples/typecheck_error/*.nbt", assert_typecheck_error); } #[test] fn runtime_error_examples_fail_as_expected() { run_for_each_file("../examples/runtime_error/*.nbt", assert_runtime_error); }