diff --git a/Cargo.lock b/Cargo.lock index dbc6e55a..83a2fd98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -19,9 +19,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -34,53 +34,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys", -] - -[[package]] -name = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "arbitrary" -version = "1.1.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7924531f38b1970ff630f03eb20a2fde69db5c590c93b0f3482e95dcc5fd60" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -103,9 +94,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.3" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" @@ -115,15 +106,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bech32" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bitcoin" -version = "0.32.3" +version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0032b0e8ead7074cda7fc4f034409607e3f03a6f71d66ade8a307f79b4d99e73" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", "bech32", @@ -144,9 +135,9 @@ checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" [[package]] name = "bitcoin-io" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-private" @@ -165,31 +156,37 @@ dependencies = [ [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", ] +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -199,9 +196,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chumsky" @@ -210,7 +207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acc17a6284abccac6e50db35c1cee87f605474a72939b959a3a67d9371800efd" dependencies = [ "hashbrown", - "regex-automata", + "regex-automata 0.3.9", "serde", "stacker", "unicode-ident", @@ -219,18 +216,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -240,9 +237,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "codegen" @@ -253,32 +250,32 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "derive_arbitrary" -version = "1.1.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a577516173adb681466d517d39bd468293bc2c2a16439375ef0f35bba45f3d" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elements" -version = "0.25.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739a0201c8b2d1e35e6509872ddb8250dd37b38d2a462b9cea05988bf9630196" +checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" dependencies = [ "bech32", "bitcoin", @@ -291,6 +288,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -305,9 +318,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -316,6 +329,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "ghost-cell" version = "0.2.6" @@ -335,9 +360,9 @@ dependencies = [ [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] @@ -350,9 +375,9 @@ checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -365,115 +390,122 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", ] [[package]] -name = "log" -version = "0.4.22" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "memchr" -version = "2.6.3" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniscript" -version = "12.3.1" +version = "12.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82911d2fb527bb9aacd2446d2f517aff3f8e3846ace1b3c24258b61ea3cce2bc" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" dependencies = [ "bech32", "bitcoin", ] -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.28" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" dependencies = [ - "ar_archive_writer", "cc", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -501,30 +533,41 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] name = "regex" -version = "1.9.5" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", ] [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.10", ] [[package]] @@ -534,10 +577,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] -name = "ryu" -version = "1.0.15" +name = "regex-syntax" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "santiago" @@ -592,33 +654,45 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] @@ -637,7 +711,7 @@ dependencies = [ "bitcoin_hashes", "byteorder", "elements", - "getrandom", + "getrandom 0.2.17", "ghost-cell", "hex-conservative", "miniscript", @@ -647,9 +721,9 @@ dependencies = [ [[package]] name = "simplicity-sys" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875630d128f19818161cefe0a3d910b6aae921d8246711db574a689cb2c11747" +checksum = "e3401ee7331f183a5458c0f5a4b3d5d00bde0fd12e2e03728c537df34efae289" dependencies = [ "bitcoin_hashes", "cc", @@ -660,16 +734,17 @@ name = "simplicityhl" version = "0.4.1" dependencies = [ "arbitrary", - "base64 0.21.3", + "base64 0.21.7", "chumsky", "clap", "either", - "getrandom", + "getrandom 0.2.17", "itertools", "miniscript", "serde", "serde_json", "simplicity-lang", + "tempfile", ] [[package]] @@ -686,15 +761,15 @@ dependencies = [ [[package]] name = "stacker" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -705,9 +780,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -715,21 +790,23 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.31" +name = "tempfile" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -745,40 +822,37 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.92" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "wasm-bindgen" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ - "bumpalo", - "log", + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn 2.0.31", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -786,22 +860,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.31", - "wasm-bindgen-backend", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -812,6 +895,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -875,3 +967,35 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index c3a8cc30..d9d0f162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,9 @@ arbitrary = { version = "1", optional = true, features = ["derive"] } clap = "4.5.37" chumsky = "0.11.2" +[dev-dependencies] +tempfile = "3" + [target.wasm32-unknown-unknown.dependencies] getrandom = { version = "0.2", features = ["js"] } @@ -68,7 +71,7 @@ copy_iterator = "warn" default_trait_access = "warn" doc_link_with_quotes = "warn" doc_markdown = "warn" -empty_enum = "warn" +empty_enums = "warn" enum_glob_use = "allow" expl_impl_clone_on_copy = "warn" explicit_deref_methods = "warn" @@ -152,7 +155,7 @@ struct_field_names = "warn" too_many_lines = "allow" transmute_ptr_to_ptr = "warn" trivially_copy_pass_by_ref = "warn" -unchecked_duration_subtraction = "warn" +unchecked_time_subtraction = "warn" unicode_not_nfc = "warn" unnecessary_box_returns = "warn" unnecessary_join = "warn" diff --git a/examples/multiple_libs/main.simf b/examples/multiple_libs/main.simf new file mode 100644 index 00000000..cead0852 --- /dev/null +++ b/examples/multiple_libs/main.simf @@ -0,0 +1,16 @@ +use merkle::build_root::{get_root, hash as and_hash}; +use base_math::simple_op::hash as or_hash; + +pub fn get_block_value_hash(prev_hash: u32, tx1: u32, tx2: u32) -> u32 { + let root: u32 = get_root(tx1, tx2); + or_hash(prev_hash, root) +} + +fn main() { + let block_val_hash: u32 = get_block_value_hash(5, 10, 20); + assert!(jet::eq_32(block_val_hash, 27)); + + let first_value: u32 = 15; + let second_value: u32 = 22; + assert!(jet::eq_32(and_hash(first_value, second_value), 6)); +} \ No newline at end of file diff --git a/examples/multiple_libs/math/simple_op.simf b/examples/multiple_libs/math/simple_op.simf new file mode 100644 index 00000000..b152a361 --- /dev/null +++ b/examples/multiple_libs/math/simple_op.simf @@ -0,0 +1,3 @@ +pub fn hash(x: u32, y: u32) -> u32 { + jet::xor_32(x, y) +} \ No newline at end of file diff --git a/examples/multiple_libs/merkle/build_root.simf b/examples/multiple_libs/merkle/build_root.simf new file mode 100644 index 00000000..f41c37e4 --- /dev/null +++ b/examples/multiple_libs/merkle/build_root.simf @@ -0,0 +1,9 @@ +use math::simple_op::hash as temp_hash; + +pub fn get_root(tx1: u32, tx2: u32) -> u32 { + temp_hash(tx1, tx2) +} + +pub fn hash(tx1: u32, tx2: u32) -> u32 { + jet::and_32(tx1, tx2) +} \ No newline at end of file diff --git a/examples/simple_multilib/crypto/hashes.simf b/examples/simple_multilib/crypto/hashes.simf new file mode 100644 index 00000000..0e463d89 --- /dev/null +++ b/examples/simple_multilib/crypto/hashes.simf @@ -0,0 +1,5 @@ +pub fn sha256(data: u32) -> u256 { + let ctx: Ctx8 = jet::sha_256_ctx_8_init(); + let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, data); + jet::sha_256_ctx_8_finalize(ctx) +} diff --git a/examples/simple_multilib/main.simf b/examples/simple_multilib/main.simf new file mode 100644 index 00000000..81031d4c --- /dev/null +++ b/examples/simple_multilib/main.simf @@ -0,0 +1,7 @@ +use math::arithmetic::add; +use crypto::hashes::sha256; + +fn main() { + let sum: u32 = add(2, 3); + let hash: u256 = sha256(sum); +} \ No newline at end of file diff --git a/examples/simple_multilib/math/arithmetic.simf b/examples/simple_multilib/math/arithmetic.simf new file mode 100644 index 00000000..2f348e0c --- /dev/null +++ b/examples/simple_multilib/math/arithmetic.simf @@ -0,0 +1,4 @@ +pub fn add(a: u32, b: u32) -> u32 { + let (_, res): (bool, u32) = jet::add_32(a, b); + res +} \ No newline at end of file diff --git a/examples/single_lib/main.simf b/examples/single_lib/main.simf new file mode 100644 index 00000000..7897ab8a --- /dev/null +++ b/examples/single_lib/main.simf @@ -0,0 +1,11 @@ +pub use temp::constants::utils::two as smth; +use temp::funcs::{get_five, Smth}; + +fn seven() -> u32 { + 7 +} + +fn main() { + let (_, temp): (bool, u32) = jet::add_32(smth(), get_five()); + assert!(jet::eq_32(temp, seven())); +} \ No newline at end of file diff --git a/examples/single_lib/temp/constants/utils.simf b/examples/single_lib/temp/constants/utils.simf new file mode 100644 index 00000000..aa5cbb53 --- /dev/null +++ b/examples/single_lib/temp/constants/utils.simf @@ -0,0 +1,5 @@ +pub use temp::funcs::Smth; + +pub fn two() -> Smth { + 2 +} \ No newline at end of file diff --git a/examples/single_lib/temp/funcs.simf b/examples/single_lib/temp/funcs.simf new file mode 100644 index 00000000..0ff2da55 --- /dev/null +++ b/examples/single_lib/temp/funcs.simf @@ -0,0 +1,5 @@ +pub type Smth = u32; + +pub fn get_five() -> u32 { + 5 +} \ No newline at end of file diff --git a/examples/temp/libs/lib/math.simf b/examples/temp/libs/lib/math.simf new file mode 100644 index 00000000..288e6d61 --- /dev/null +++ b/examples/temp/libs/lib/math.simf @@ -0,0 +1 @@ +fn add(a: u32, b: u32) {} \ No newline at end of file diff --git a/examples/temp/main.simf b/examples/temp/main.simf new file mode 100644 index 00000000..55817e4a --- /dev/null +++ b/examples/temp/main.simf @@ -0,0 +1 @@ +use lib::math::add; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/lib/b.simf b/functional-tests/error-test-cases/cross-wire/lib/b.simf new file mode 100644 index 00000000..be3150c7 --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/lib/b.simf @@ -0,0 +1,2 @@ +use lib::x::TypeX; +use lib::y::TypeY; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/lib/c.simf b/functional-tests/error-test-cases/cross-wire/lib/c.simf new file mode 100644 index 00000000..6efaf9a9 --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/lib/c.simf @@ -0,0 +1,2 @@ +use lib::y::TypeY; +use lib::x::TypeX; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/lib/x.simf b/functional-tests/error-test-cases/cross-wire/lib/x.simf new file mode 100644 index 00000000..8678e1df --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/lib/x.simf @@ -0,0 +1 @@ +pub type TypeX = u32; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/lib/y.simf b/functional-tests/error-test-cases/cross-wire/lib/y.simf new file mode 100644 index 00000000..5c4c8091 --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/lib/y.simf @@ -0,0 +1 @@ +pub type TypeY = u64; \ No newline at end of file diff --git a/functional-tests/error-test-cases/cross-wire/main.simf b/functional-tests/error-test-cases/cross-wire/main.simf new file mode 100644 index 00000000..afddec8c --- /dev/null +++ b/functional-tests/error-test-cases/cross-wire/main.simf @@ -0,0 +1,4 @@ +use lib::b::TypeX; +use lib::c::TypeY; + +fn main() {} \ No newline at end of file diff --git a/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf b/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf new file mode 100644 index 00000000..1d07a968 --- /dev/null +++ b/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf @@ -0,0 +1,2 @@ +pub use lib::module_b::TypeB; +pub type TypeA = u32; diff --git a/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf b/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf new file mode 100644 index 00000000..2b340054 --- /dev/null +++ b/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf @@ -0,0 +1,2 @@ +pub use lib::module_a::TypeA; +pub type TypeB = u32; diff --git a/functional-tests/error-test-cases/cyclic-dependency/main.simf b/functional-tests/error-test-cases/cyclic-dependency/main.simf new file mode 100644 index 00000000..338a4030 --- /dev/null +++ b/functional-tests/error-test-cases/cyclic-dependency/main.simf @@ -0,0 +1,3 @@ +use lib::module_a::TypeA; + +fn main() {} diff --git a/functional-tests/error-test-cases/file-not-found/main.simf b/functional-tests/error-test-cases/file-not-found/main.simf new file mode 100644 index 00000000..2d016d20 --- /dev/null +++ b/functional-tests/error-test-cases/file-not-found/main.simf @@ -0,0 +1,5 @@ +use lib::module::AssetId; + +fn main() { + let my_asset: AssetId = 5; +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/global/lib/module.simf b/functional-tests/error-test-cases/global/lib/module.simf new file mode 100644 index 00000000..d87a0da8 --- /dev/null +++ b/functional-tests/error-test-cases/global/lib/module.simf @@ -0,0 +1,2 @@ +pub type AssetId = u32; +pub fn get_id() -> AssetId { 1 } \ No newline at end of file diff --git a/functional-tests/error-test-cases/global/main.simf b/functional-tests/error-test-cases/global/main.simf new file mode 100644 index 00000000..f0fb986c --- /dev/null +++ b/functional-tests/error-test-cases/global/main.simf @@ -0,0 +1,5 @@ +use lib::module::*; + +fn main() { + let my_asset: AssetId = 5; +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/lib-not-found/main.simf b/functional-tests/error-test-cases/lib-not-found/main.simf new file mode 100644 index 00000000..2d016d20 --- /dev/null +++ b/functional-tests/error-test-cases/lib-not-found/main.simf @@ -0,0 +1,5 @@ +use lib::module::AssetId; + +fn main() { + let my_asset: AssetId = 5; +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/name-collision/lib/groups.simf b/functional-tests/error-test-cases/name-collision/lib/groups.simf new file mode 100644 index 00000000..7ef2eff1 --- /dev/null +++ b/functional-tests/error-test-cases/name-collision/lib/groups.simf @@ -0,0 +1,3 @@ +pub fn add(a: u32, b: u32) -> (bool, u32) { + jet::add_32(a, b) +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/name-collision/lib/math.simf b/functional-tests/error-test-cases/name-collision/lib/math.simf new file mode 100644 index 00000000..b9a83fd9 --- /dev/null +++ b/functional-tests/error-test-cases/name-collision/lib/math.simf @@ -0,0 +1,4 @@ +pub fn add(a: u32, b: u32) -> (bool, u32) { + let (_, c): (bool, u32) = jet::add_32(a, b); + jet::add_32(c, 1) +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/name-collision/main.simf b/functional-tests/error-test-cases/name-collision/main.simf new file mode 100644 index 00000000..59b5c813 --- /dev/null +++ b/functional-tests/error-test-cases/name-collision/main.simf @@ -0,0 +1,9 @@ +use lib::groups::add; +use lib::math::add; + +fn main() { + let x: u32 = 5; + let y: u32 = 10; + let (_, result): (bool, u32) = add(x, y); + assert!(jet::eq_32(result, 16)); +} \ No newline at end of file diff --git a/functional-tests/error-test-cases/private-visibility/lib/hidden.simf b/functional-tests/error-test-cases/private-visibility/lib/hidden.simf new file mode 100644 index 00000000..70da82d4 --- /dev/null +++ b/functional-tests/error-test-cases/private-visibility/lib/hidden.simf @@ -0,0 +1 @@ +type SecretType = u32; pub fn ok() {} \ No newline at end of file diff --git a/functional-tests/error-test-cases/private-visibility/main.simf b/functional-tests/error-test-cases/private-visibility/main.simf new file mode 100644 index 00000000..e1eb9dd8 --- /dev/null +++ b/functional-tests/error-test-cases/private-visibility/main.simf @@ -0,0 +1,3 @@ +use lib::hidden::SecretType; + +fn main() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf new file mode 100644 index 00000000..60a6f1d4 --- /dev/null +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf @@ -0,0 +1,2 @@ +pub use lib::level2::CoreSmth; +pub use lib::level2::core_val; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf new file mode 100644 index 00000000..7a32e9a0 --- /dev/null +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf @@ -0,0 +1,2 @@ +pub use lib::level3::CoreSmth; +pub use lib::level3::core_val; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf new file mode 100644 index 00000000..6f0b9983 --- /dev/null +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf @@ -0,0 +1,2 @@ +pub type CoreSmth = u32; +pub fn core_val() -> CoreSmth { 42 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/main.simf b/functional-tests/valid-test-cases/deep-reexport-chain/main.simf new file mode 100644 index 00000000..bd15f456 --- /dev/null +++ b/functional-tests/valid-test-cases/deep-reexport-chain/main.simf @@ -0,0 +1,7 @@ +use lib::level1::CoreSmth; +use lib::level1::core_val; + +fn main() { + let val: CoreSmth = core_val(); + assert!(jet::eq_32(val, 42)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf new file mode 100644 index 00000000..ad05850a --- /dev/null +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf @@ -0,0 +1 @@ +pub type BaseType = u32; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf new file mode 100644 index 00000000..b1bd0652 --- /dev/null +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf @@ -0,0 +1,2 @@ +pub use lib::base::BaseType; +pub fn get_left() -> BaseType { 1 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf new file mode 100644 index 00000000..629deab8 --- /dev/null +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf @@ -0,0 +1,2 @@ +pub use lib::base::BaseType; +pub fn get_right() -> BaseType { 2 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf new file mode 100644 index 00000000..cef4de5c --- /dev/null +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf @@ -0,0 +1,9 @@ +use lib::left::get_left; +use lib::right::get_right; + +fn main() { + let a: BaseType = get_left(); + let b: BaseType = get_right(); + let (_, c): (bool, BaseType) = jet::add_32(a, b); + assert!(jet::eq_32(c, 3)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf b/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf new file mode 100644 index 00000000..5b33d6b9 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf @@ -0,0 +1,6 @@ +use types::def::UserId; +use db::store::get_record; + +pub fn is_valid(id: UserId) -> UserId { + get_record(id) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf b/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf new file mode 100644 index 00000000..20a6cc61 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf @@ -0,0 +1,2 @@ +use types::def::UserId; +pub fn get_record(id: UserId) -> UserId { id } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/main.simf b/functional-tests/valid-test-cases/interleaved-waterfall/main.simf new file mode 100644 index 00000000..0abb7218 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/main.simf @@ -0,0 +1,5 @@ +use orch::handler::run_system; + +fn main() { + assert!(jet::eq_32(run_system(5), 5)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf b/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf new file mode 100644 index 00000000..86d627d6 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf @@ -0,0 +1,8 @@ +use auth::verify::is_valid; +use db::store::get_record; +use types::def::UserId; + +pub fn run_system(id: UserId) -> UserId { + let checked: UserId = is_valid(id); + get_record(checked) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf b/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf new file mode 100644 index 00000000..a8fd5650 --- /dev/null +++ b/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf @@ -0,0 +1 @@ +pub type UserId = u32; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/keyword-as-lib/jet/fn/let.simf b/functional-tests/valid-test-cases/keyword-as-lib/jet/fn/let.simf new file mode 100644 index 00000000..1ffbb5fa --- /dev/null +++ b/functional-tests/valid-test-cases/keyword-as-lib/jet/fn/let.simf @@ -0,0 +1,3 @@ +pub fn get_some_value() -> u64 { + 1 +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/keyword-as-lib/main.simf b/functional-tests/valid-test-cases/keyword-as-lib/main.simf new file mode 100644 index 00000000..80143ab6 --- /dev/null +++ b/functional-tests/valid-test-cases/keyword-as-lib/main.simf @@ -0,0 +1,3 @@ +use r#jet::r#fn::r#let; + +pub fn main() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf b/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf new file mode 100644 index 00000000..2f11c01e --- /dev/null +++ b/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf @@ -0,0 +1,5 @@ +type SecretKey = u64; + +pub fn unlock(key: SecretKey) -> u64 { + jet::max_64(key, 0) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/leaky-signature/main.simf b/functional-tests/valid-test-cases/leaky-signature/main.simf new file mode 100644 index 00000000..77592ac0 --- /dev/null +++ b/functional-tests/valid-test-cases/leaky-signature/main.simf @@ -0,0 +1,4 @@ +use lib::internal::unlock; + +fn main() { +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-simple/lib/module.simf b/functional-tests/valid-test-cases/module-simple/lib/module.simf new file mode 100644 index 00000000..d5cfec25 --- /dev/null +++ b/functional-tests/valid-test-cases/module-simple/lib/module.simf @@ -0,0 +1 @@ +pub fn add() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-simple/main.simf b/functional-tests/valid-test-cases/module-simple/main.simf new file mode 100644 index 00000000..bb2705df --- /dev/null +++ b/functional-tests/valid-test-cases/module-simple/main.simf @@ -0,0 +1,2 @@ +use lib::module::add; +fn main() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf b/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf new file mode 100644 index 00000000..d80ca18a --- /dev/null +++ b/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf @@ -0,0 +1,3 @@ +pub use crypto::crypto::mock_hash; +pub use math::math::MathInt; +pub use math::math::add_two; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf b/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf new file mode 100644 index 00000000..51111aca --- /dev/null +++ b/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf @@ -0,0 +1,5 @@ +use math::math::MathInt; + +pub fn mock_hash(x: MathInt) -> (bool, MathInt) { + jet::add_32(x, 5) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/main.simf b/functional-tests/valid-test-cases/multi-lib-facade/main.simf new file mode 100644 index 00000000..44ff5c3c --- /dev/null +++ b/functional-tests/valid-test-cases/multi-lib-facade/main.simf @@ -0,0 +1,8 @@ +use api::api::{add_two, mock_hash, MathInt}; + +fn main() { + let val: MathInt = 10; + let (_, step1): (bool, MathInt) = add_two(val); + let (_, step2): (bool, MathInt) = mock_hash(step1); + assert!(jet::eq_32(step2, 17)); +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf b/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf new file mode 100644 index 00000000..d8d16499 --- /dev/null +++ b/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf @@ -0,0 +1,4 @@ +pub type MathInt = u32; +pub fn add_two(x: MathInt) -> (bool, MathInt) { + jet::add_32(x, 2) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf new file mode 100644 index 00000000..71526bed --- /dev/null +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf @@ -0,0 +1,2 @@ +pub type Coin = u64; +pub fn mint(val: u64) -> Coin { val } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf new file mode 100644 index 00000000..7571e9e3 --- /dev/null +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf @@ -0,0 +1,2 @@ +pub use lib::core::Coin; +pub use lib::core::mint; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf new file mode 100644 index 00000000..c7f4b74b --- /dev/null +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf @@ -0,0 +1,5 @@ +pub use lib::core::Coin; + +pub fn burn(c: Coin) -> (bool, Coin) { + jet::subtract_64(c, 1) +} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/main.simf b/functional-tests/valid-test-cases/reexport-diamond/main.simf new file mode 100644 index 00000000..799b96e6 --- /dev/null +++ b/functional-tests/valid-test-cases/reexport-diamond/main.simf @@ -0,0 +1,9 @@ +use lib::route_a::Coin; +use lib::route_a::mint; +use lib::route_b::burn; + +fn main() { + let my_coin: Coin = mint(10); + let (_, remaining): (bool, Coin) = burn(my_coin); + assert!(jet::eq_64(remaining, 9)); +} \ No newline at end of file diff --git a/src/ast.rs b/src/ast.rs index 3c59f2f7..6b0ade46 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -9,6 +9,7 @@ use miniscript::iter::{Tree, TreeLike}; use simplicity::jet::Elements; use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; +use crate::driver::{AliasRegistry, IdentifierWithFileID, ProgramResolutions}; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::parse::MatchPattern; @@ -19,7 +20,7 @@ use crate::types::{ }; use crate::value::{UIntValue, Value}; use crate::witness::{Parameters, WitnessTypes, WitnessValues}; -use crate::{impl_eq_hash, parse}; +use crate::{driver, impl_eq_hash, parse, SourceName}; /// A program consists of the main function. /// @@ -520,18 +521,66 @@ impl TreeLike for ExprTree<'_> { /// 2. Resolving type aliases /// 3. Assigning types to each witness expression /// 4. Resolving calls to custom functions -#[derive(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq)] struct Scope { + resolutions: ProgramResolutions, + paths: Arc<[SourceName]>, + import_aliases: AliasRegistry, + file_id: usize, // ID of the file from which the function is called. + variables: Vec>, aliases: HashMap, parameters: HashMap, witnesses: HashMap, - functions: HashMap, + functions: HashMap, is_main: bool, call_tracker: CallTracker, } +impl Default for Scope { + fn default() -> Self { + Self { + resolutions: Arc::from([]), + paths: Arc::from([]), + import_aliases: AliasRegistry::default(), + file_id: 0, + variables: Vec::new(), + aliases: HashMap::new(), + parameters: HashMap::new(), + witnesses: HashMap::new(), + functions: HashMap::new(), + is_main: false, + call_tracker: CallTracker::default(), + } + } +} + impl Scope { + pub fn new( + resolutions: ProgramResolutions, + paths: Arc<[SourceName]>, + import_aliases: AliasRegistry, + ) -> Self { + Self { + resolutions, + paths, + import_aliases, + file_id: 0, + variables: Vec::new(), + aliases: HashMap::new(), + parameters: HashMap::new(), + witnesses: HashMap::new(), + functions: HashMap::new(), + is_main: false, + call_tracker: CallTracker::default(), + } + } + + /// Access to current function file id. + pub fn file_id(&self) -> usize { + self.file_id + } + /// Check if the current scope is topmost. pub fn is_topmost(&self) -> bool { self.variables.is_empty() @@ -542,6 +591,11 @@ impl Scope { self.variables.push(HashMap::new()); } + pub fn push_function_scope(&mut self, file_id: usize) { + self.push_scope(); + self.file_id = file_id; + } + /// Push the scope of the main function onto the stack. /// /// ## Panics @@ -564,6 +618,11 @@ impl Scope { self.variables.pop().expect("Stack is empty"); } + pub fn pop_function_scope(&mut self, previous_file_id: usize) { + self.pop_scope(); + self.file_id = previous_file_id; + } + /// Pop the scope of the main function from the stack. /// /// ## Panics @@ -682,20 +741,82 @@ impl Scope { pub fn insert_function( &mut self, name: FunctionName, + file_id: usize, function: CustomFunction, ) -> Result<(), Error> { - match self.functions.entry(name.clone()) { - Entry::Occupied(_) => Err(Error::FunctionRedefined(name)), - Entry::Vacant(entry) => { - entry.insert(function); - Ok(()) - } + let global_id = (name.clone().into(), file_id); + + if self.functions.contains_key(&global_id) { + return Err(Error::FunctionRedefined(name)); } + + let _ = self.functions.insert(global_id, function); + Ok(()) + + // match self.functions.entry(global_id) { + // Entry::Occupied(_) => Err(Error::FunctionRedefined(name)), + // Entry::Vacant(entry) => { + // entry.insert(function); + // Ok(()) + // } + // } } - /// Get the definition of a custom function. - pub fn get_function(&self, name: &FunctionName) -> Option<&CustomFunction> { - self.functions.get(name) + // TODO: @LesterEvSe, Consider why we use this function to get a type. + + /// Retrieves the definition of a custom function, enforcing strict error prioritization. + /// + /// # Architecture Note + /// The order of operations here is intentional to prioritize specific compiler errors: + /// 1. Resolve the alias to find the true global coordinates. + /// 2. Check for global existence (`FunctionUndefined`) *before* checking local visibility. + /// 3. Verify if the current file's scope is actually allowed to see it (`PrivateItem`). + /// + /// # Errors + /// + /// * [`Error::FunctionUndefined`]: The function is not found in the global registry. + /// * [`Error::FileNotFound`]: The specified `file_id` does not exist in the resolutions table. + /// * [`Error::PrivateItem`]: The function exists globally but is not exposed to the current file's scope. + pub fn get_function(&self, name: &FunctionName) -> Result<&CustomFunction, Error> { + // 1. Get the true global ID of the alias (or keep the current name if it is not aliased). + // Note: The order of the errors is important! We must know the true identify first. + let initial_id = (name.clone().into(), self.file_id); + let global_id = self + .import_aliases + .resolved_roots() + .get(&initial_id) + .cloned() + .unwrap_or(initial_id); + + // 2. Fetch the function from the global pool. + // We do this first so we can throw FunctionUndefined before checking local visibility. + let function = self + .functions + .get(&global_id) + .ok_or_else(|| Error::FunctionUndefined(name.clone()))?; + + let source_name = &self.paths[self.file_id]; + + let file_scope = match source_name { + SourceName::Real(path) => self + .resolutions + .get(self.file_id) + .ok_or_else(|| Error::FileNotFound(path.to_path_buf()))?, + SourceName::Virtual(_) => { + // Virtual sources (e.g., injected code or REPL/Tests) bypass local visibility checks. + return Ok(function); + } + }; + + // 3. Verify lcoal scope visibility. + // We successfully found the function globally, but is this file allowed to use it? + let identifier: Identifier = name.clone().into(); + + if file_scope.contains_key(&identifier) { + Ok(function) + } else { + Err(Error::PrivateItem(name.as_inner().to_string())) + } } /// Track a call expression with its span. @@ -718,9 +839,13 @@ trait AbstractSyntaxTree: Sized { } impl Program { - pub fn analyze(from: &parse::Program) -> Result { + pub fn analyze(from: &driver::Program) -> Result { let unit = ResolvedType::unit(); - let mut scope = Scope::default(); + let mut scope = Scope::new( + Arc::from(from.resolutions()), + Arc::from(from.paths()), + from.import_aliases().clone(), + ); let items = from .items() .iter() @@ -746,35 +871,37 @@ impl Program { } impl AbstractSyntaxTree for Item { - type From = parse::Item; + type From = driver::Item; fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { assert!(ty.is_unit(), "Items cannot return anything"); assert!(scope.is_topmost(), "Items live in the topmost scope only"); match from { - parse::Item::TypeAlias(alias) => { + driver::Item::TypeAlias(alias) => { scope .insert_alias(alias.name().clone(), alias.ty().clone()) .with_span(alias)?; Ok(Self::TypeAlias) } - parse::Item::Function(function) => { + driver::Item::Function(function) => { Function::analyze(function, ty, scope).map(Self::Function) } - parse::Item::Module => Ok(Self::Module), + driver::Item::Module => Ok(Self::Module), } } } impl AbstractSyntaxTree for Function { - type From = parse::Function; + type From = driver::Function; fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { assert!(ty.is_unit(), "Function definitions cannot return anything"); assert!(scope.is_topmost(), "Items live in the topmost scope only"); + let previous_file_id = scope.file_id(); if from.name().as_inner() != "main" { + let file_id = from.file_id(); let params = from .params() .iter() @@ -791,16 +918,16 @@ impl AbstractSyntaxTree for Function { .map(|aliased| scope.resolve(aliased).with_span(from)) .transpose()? .unwrap_or_else(ResolvedType::unit); - scope.push_scope(); + scope.push_function_scope(file_id); for param in params.iter() { scope.insert_variable(param.identifier().clone(), param.ty().clone()); } let body = Expression::analyze(from.body(), &ret, scope).map(Arc::new)?; - scope.pop_scope(); + scope.pop_function_scope(previous_file_id); debug_assert!(scope.is_topmost()); let function = CustomFunction { params, body }; scope - .insert_function(from.name().clone(), function) + .insert_function(from.name().clone(), file_id, function) .with_span(from)?; return Ok(Self::Custom); @@ -1321,14 +1448,9 @@ impl AbstractSyntaxTree for CallName { .get_function(name) .cloned() .map(Self::Custom) - .ok_or(Error::FunctionUndefined(name.clone())) .with_span(from), parse::CallName::ArrayFold(name, size) => { - let function = scope - .get_function(name) - .cloned() - .ok_or(Error::FunctionUndefined(name.clone())) - .with_span(from)?; + let function = scope.get_function(name).cloned().with_span(from)?; // A function that is used in a array fold has the signature: // fn f(element: E, accumulator: A) -> A if function.params().len() != 2 || function.params()[1].ty() != function.body().ty() @@ -1339,11 +1461,7 @@ impl AbstractSyntaxTree for CallName { } } parse::CallName::Fold(name, bound) => { - let function = scope - .get_function(name) - .cloned() - .ok_or(Error::FunctionUndefined(name.clone())) - .with_span(from)?; + let function = scope.get_function(name).cloned().with_span(from)?; // A function that is used in a list fold has the signature: // fn f(element: E, accumulator: A) -> A if function.params().len() != 2 || function.params()[1].ty() != function.body().ty() @@ -1354,11 +1472,7 @@ impl AbstractSyntaxTree for CallName { } } parse::CallName::ForWhile(name) => { - let function = scope - .get_function(name) - .cloned() - .ok_or(Error::FunctionUndefined(name.clone())) - .with_span(from)?; + let function = scope.get_function(name).cloned().with_span(from)?; // A function that is used in a for-while loop has the signature: // fn f(accumulator: A, readonly_context: C, counter: u{N}) -> Either // where @@ -1429,11 +1543,18 @@ impl AbstractSyntaxTree for Match { } } +/// Analyze a parsed module program to extract assignments for a specific module. +/// +/// This function searches the parsed program for a module matching the given `name`. +/// If found, it evaluates the module's assignments and returns them as a map of +/// witness names to their constant values. If the module is not present, an empty +/// map is returned. fn analyze_named_module( name: ModuleName, from: &parse::ModuleProgram, ) -> Result, RichError> { let unit = ResolvedType::unit(); + let mut scope = Scope::default(); let items = from .items() diff --git a/src/driver.rs b/src/driver.rs new file mode 100644 index 00000000..13bd0fc4 --- /dev/null +++ b/src/driver.rs @@ -0,0 +1,1757 @@ +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::fmt; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use crate::error::{Error, ErrorCollector, RichError, Span}; +use crate::parse::{self, AliasedIdentifier, ParseFromStrWithErrors, Visibility}; +use crate::str::{AliasName, FunctionName, Identifier}; +use crate::types::AliasedType; +use crate::{impl_eq_hash, DependencyMap, SourceFile, SourceName}; + +/// Represents a single, isolated file in the SimplicityHL project. +/// In this architecture, a file and a module are the exact same thing. +#[derive(Debug, Clone)] +pub struct Module { + pub source: SourceFile, + /// The completely parsed program for this specific file. + /// it contains all the functions, aliases, and imports defined inside the file. + pub parsed_program: parse::Program, +} + +pub type IdentifierWithFileID = (Identifier, usize); + +/// The Dependency Graph itself. +pub struct ProjectGraph { + /// Arena Pattern: the data itself lives here. + /// A flat vector guarantees that module data is stored contiguously in memory. + pub(self) modules: Vec, + + /// The configuration environment. + /// Used to resolve external library dependencies and invoke their associated functions. + pub dependency_map: Arc, + + /// Fast lookup: `SourceName` -> Module ID. + /// A reverse index mapping absolute file paths to their internal IDs. + /// This solves the duplication problem, ensuring each file is only parsed once. + pub lookup: HashMap, + + /// Fast lookup: Module ID -> `SourceName`. + /// A direct index mapping internal IDs back to their absolute file paths. + /// This serves as the exact inverse of the `lookup` map. + pub paths: Arc<[SourceName]>, + + /// The Adjacency List: Defines the Directed acyclic Graph (DAG) of imports. + /// + /// The Key (`usize`) is the ID of a "Parent" module (the file doing the importing). + /// The Value (`Vec`) is a list of IDs of the "Child" modules it relies on. + /// + /// Example: If `main.simf` (ID: 0) has `use lib::math;` (ID: 1) and `use lib::io;` (ID: 2), + /// this map will contain: `{ 0: [1, 2] }`. + pub dependencies: HashMap>, +} + +// TODO: @LesterEvSe, Consider to change BTreeMap to BTreeSet here +pub type FileResolutions = BTreeMap; + +pub type ProgramResolutions = Arc<[FileResolutions]>; + +/// A standard mapping from one unique identifier to another +pub type AliasMap = BTreeMap; + +/// Manages the resolution of import aliases across the entire program. +#[derive(Clone, Debug, Default)] +pub struct AliasRegistry { + /// Maps an alias to its immediate target. + /// (e.g., `use B as C;` stores C -> B) + pub(self) direct_targets: AliasMap, + + /// Caches the final, original definition of an alias to avoid walking the chain. + /// (e.g., If C -> B and B -> A, this stores C -> A) + pub(self) resolved_roots: AliasMap, +} + +impl AliasRegistry { + /// Access the direct targets of the `AliasRegistry` + pub fn direct_targets(&self) -> &AliasMap { + &self.direct_targets + } + + /// Access the resolved roots of the `AliasRegistry` + pub fn resolved_roots(&self) -> &AliasMap { + &self.resolved_roots + } +} + +impl_eq_hash!(AliasRegistry; direct_targets, resolved_roots); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Resolution { + pub visibility: Visibility, +} + +#[derive(Clone, Debug)] +pub struct Program { + items: Arc<[Item]>, + paths: Arc<[SourceName]>, + + import_aliases: AliasRegistry, + + resolutions: ProgramResolutions, + span: Span, +} + +impl Program { + pub fn from_parse( + parsed: &parse::Program, + source: SourceFile, + handler: &mut ErrorCollector, + ) -> Option { + let root_path = source.name().without_extension(); + + let mut items: Vec = Vec::new(); + let mut resolutions: Vec = vec![BTreeMap::new()]; + + let main_file_id = 0usize; + let mut errors: Vec = Vec::new(); + + for item in parsed.items() { + match item { + parse::Item::Use(use_decl) => { + let bug_report = RichError::new( + Error::UnknownLibrary(use_decl.path_buf().to_string_lossy().to_string()), + *use_decl.span(), + ); + handler.push(bug_report); + } + parse::Item::TypeAlias(alias) => { + if let Some(err) = ProjectGraph::register_def( + &mut items, + &mut resolutions, + main_file_id, + item, + alias.name().clone().into(), + &parse::Visibility::Public, + ) { + errors.push(err) + } + } + parse::Item::Function(function) => { + if let Some(err) = ProjectGraph::register_def( + &mut items, + &mut resolutions, + main_file_id, + item, + function.name().clone().into(), + &parse::Visibility::Public, + ) { + errors.push(err); + } + } + parse::Item::Module => {} + } + } + handler.update_with_source_enrichment(source, errors); + + if handler.has_errors() { + None + } else { + Some(Program { + items: items.into(), + paths: Arc::from([root_path]), + import_aliases: AliasRegistry::default(), + resolutions: resolutions.into(), + span: *parsed.as_ref(), + }) + } + } + + /// Access the items of the Program. + pub fn items(&self) -> &[Item] { + &self.items + } + + /// Access the paths of the Program. + pub fn paths(&self) -> &[SourceName] { + &self.paths + } + + /// Access the import aliases of the Program. + pub fn import_aliases(&self) -> &AliasRegistry { + &self.import_aliases + } + + /// Access the scope items of the Program. + pub fn resolutions(&self) -> &[FileResolutions] { + &self.resolutions + } +} + +impl_eq_hash!(Program; items, paths, import_aliases, resolutions); + +/// An item is a component of a driver Program +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum Item { + /// A type alias. + TypeAlias(TypeAlias), + /// A function. + Function(Function), + /// A module, which is ignored. + Module, +} + +impl Item { + pub fn from_parse(parsed: &parse::Item, file_id: usize) -> Result { + match parsed { + parse::Item::TypeAlias(alias) => { + let driver_alias = TypeAlias::from_parse(alias); + Ok(Item::TypeAlias(driver_alias)) + } + parse::Item::Function(func) => { + let driver_func = Function::from_parse(func, file_id); + Ok(Item::Function(driver_func)) + } + parse::Item::Module => Ok(Item::Module), + parse::Item::Use(use_decl) => { + Err(RichError::new( + Error::Internal("Encountered 'Use' item during driver generation. Imports should be resolved by ProjectGraph.".to_string()), + *use_decl.span(), + )) + }, + } + } +} + +/// Definition of a function. +#[derive(Clone, Debug)] +pub struct Function { + file_id: usize, + name: FunctionName, + params: Arc<[parse::FunctionParam]>, + ret: Option, + body: parse::Expression, + span: Span, +} + +impl Function { + /// Converts a parser function to a driver function. + /// + /// We explicitly pass `file_id` here because the `parse::Function` + /// doesn't know which file it came from. + pub fn from_parse(parsed: &parse::Function, file_id: usize) -> Self { + Self { + file_id, + name: parsed.name().clone(), + params: Arc::from(parsed.params()), + ret: parsed.ret().cloned(), + body: parsed.body().clone(), + span: *parsed.as_ref(), + } + } + + /// Access the file id of the function. + pub fn file_id(&self) -> usize { + self.file_id + } + + /// Access the name of the function. + pub fn name(&self) -> &FunctionName { + &self.name + } + + /// Access the parameters of the function. + pub fn params(&self) -> &[parse::FunctionParam] { + &self.params + } + + /// Access the return type of the function. + /// + /// An empty return type means that the function returns the unit value. + pub fn ret(&self) -> Option<&AliasedType> { + self.ret.as_ref() + } + + /// Access the body of the function. + pub fn body(&self) -> &parse::Expression { + &self.body + } + + /// Access the span of the function. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(Function; file_id, name, params, ret, body); + +// A type alias. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct TypeAlias { + name: AliasName, + ty: AliasedType, + span: Span, +} + +impl TypeAlias { + /// Converts a parser function to a driver function. + /// + /// We explicitly pass `file_id` here because the `parse::Function` + /// doesn't know which file it came from. + pub fn from_parse(parsed: &parse::TypeAlias) -> Self { + Self { + name: parsed.name().clone(), + ty: parsed.ty().clone(), + span: *parsed.as_ref(), + } + } + + /// Access the name of the alias. + pub fn name(&self) -> &AliasName { + &self.name + } + + /// Access the type that the alias resolves to. + /// + /// During the parsing stage, the resolved type may include aliases. + /// The compiler will later check if all contained aliases have been declared before. + pub fn ty(&self) -> &AliasedType { + &self.ty + } + + /// Access the span of the alias. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(TypeAlias; name, ty); + +#[derive(Debug)] +pub enum C3Error { + CycleDetected(Vec), + /// Error for inconsistent MRO. + /// This can happen if the dependency graph has a shape that makes the + /// order of parent classes ambiguous. + /// Example: A depends on B and C, and B also depends on C. + /// The linearization of A is A + merge(linearization(B), linearization(C), [B, C]). + /// If B appears before C in one parent's linearization but C appears before B + /// in another's, the merge will fail. + InconsistentLinearization { + module: String, + conflicts: Vec>, + }, +} + +impl fmt::Display for C3Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + C3Error::CycleDetected(cycle) => { + write!(f, "Circular dependency detected: {:?}", cycle.join(" -> ")) + } + C3Error::InconsistentLinearization { module, conflicts } => { + writeln!(f, "Inconsistent resolution order for module '{}'", module)?; + writeln!( + f, + "The compiler could not resolve the following conflicting import constraints:" + )?; + + // Loop through the matrix and print each conflicting sequence + for conflict in conflicts { + writeln!(f, " [{}]", conflict.join(", "))?; + } + + write!( + f, + "Try reordering your `use` statements to avoid cross-wiring." + ) + } + } + } +} + +impl ProjectGraph { + fn parse_and_get_program( + full_path: &Path, + importer_source: SourceFile, + span: Span, + handler: &mut ErrorCollector, + ) -> Option { + let dep_key = SourceName::Real(Arc::from(full_path.with_extension(""))); + let Ok(content) = std::fs::read_to_string(full_path) else { + let err = RichError::new(Error::FileNotFound(PathBuf::from(full_path)), span) + .with_source(importer_source.clone()); + + handler.push(err); + return None; + }; + + let dep_source_file = SourceFile::new(dep_key.clone(), Arc::from(content.clone())); + + parse::Program::parse_from_str_with_errors(&content, dep_source_file.clone(), handler).map( + |parsed_program| Module { + source: dep_source_file, + parsed_program, + }, + ) + } + + pub fn new( + root_source: SourceFile, + dependency_map: Arc, + root_program: &parse::Program, + handler: &mut ErrorCollector, + ) -> Option { + let root_name_no_ext = root_source.name().without_extension(); + + let mut modules: Vec = vec![Module { + source: root_source, + parsed_program: root_program.clone(), + }]; + + let mut lookup: HashMap = HashMap::new(); + let mut paths: Vec = vec![root_name_no_ext.clone()]; + let mut dependencies: HashMap> = HashMap::new(); + + let root_id = 0; + lookup.insert(root_name_no_ext, root_id); + dependencies.insert(root_id, Vec::new()); + + // Implementation of the standard BFS algorithm with memoization and queue + let mut queue = VecDeque::new(); + queue.push_back(root_id); + + while let Some(curr_id) = queue.pop_front() { + // We need this to report errors inside THIS file. + let importer_source = modules[curr_id].source.clone(); + let current_program = &modules[curr_id].parsed_program; + + // Lists to separate valid logic from errors + let mut valid_imports: Vec<(PathBuf, Span)> = Vec::new(); + let mut resolution_errors: Vec = Vec::new(); + + // PHASE 1: Resolve Imports + for elem in current_program.items() { + if let parse::Item::Use(use_decl) = elem { + match resolve_single_import(importer_source.clone(), use_decl, &dependency_map) + { + Ok(path) => valid_imports.push((path, *use_decl.span())), + Err(err) => resolution_errors.push(err), + } + } + } + + // Phase 2: Load and Parse Dependencies + for (path, import_span) in valid_imports { + let full_path = path.with_extension("simf"); + let dep_source_name = SourceName::Real(Arc::from(full_path.as_path())); + let dep_key = dep_source_name.without_extension(); + + if let Some(&existing_id) = lookup.get(&dep_key) { + let deps = dependencies.entry(curr_id).or_default(); + if !deps.contains(&existing_id) { + deps.push(existing_id); + } + continue; + } + + let Some(module) = ProjectGraph::parse_and_get_program( + &full_path, + importer_source.clone(), + import_span, + handler, + ) else { + continue; + }; + + let last_ind = modules.len(); + modules.push(module); + + lookup.insert(dep_key.clone(), last_ind); + paths.push(dep_key); + dependencies.entry(curr_id).or_default().push(last_ind); + + queue.push_back(last_ind); + } + } + + if handler.has_errors() { + None + } else { + Some(Self { + modules, + dependency_map, + lookup, + paths: paths.into(), + dependencies, + }) + } + } + + pub fn c3_linearize(&self) -> Result, C3Error> { + self.linearize_module(0) + } + + fn linearize_module(&self, root: usize) -> Result, C3Error> { + let mut memo = HashMap::>::new(); + let mut visiting = Vec::::new(); + + self.linearize_rec(root, &mut memo, &mut visiting) + } + + fn linearize_rec( + &self, + module: usize, + memo: &mut HashMap>, + visiting: &mut Vec, + ) -> Result, C3Error> { + if let Some(result) = memo.get(&module) { + return Ok(result.clone()); + } + + if visiting.contains(&module) { + let cycle_start = visiting.iter().position(|m| *m == module).unwrap(); + let cycle_names: Vec = visiting[cycle_start..] + .iter() + .map(|&id| self.modules[id].source.name().to_string()) + .collect(); + return Err(C3Error::CycleDetected(cycle_names)); + } + + visiting.push(module); + + let parents = self.dependencies.get(&module).cloned().unwrap_or_default(); + + let mut seqs: Vec> = Vec::new(); + + for parent in &parents { + let line = self.linearize_rec(*parent, memo, visiting)?; + seqs.push(line); + } + + let mut result = vec![module]; + let merged = match merge(seqs) { + Ok(m) => m, + Err(conflicts) => { + // Map the failing usize sequences into readable module names + let conflict_names: Vec> = conflicts + .into_iter() + .map(|seq| { + seq.into_iter() + .map(|id| self.modules[id].source.name().to_string()) + .collect() + }) + .collect(); + + return Err(C3Error::InconsistentLinearization { + module: self.modules[module].source.name().to_string(), + conflicts: conflict_names, + }); + } + }; + + result.extend(merged); + + visiting.pop(); + memo.insert(module, result.clone()); + + Ok(result) + } + + /// Processes a single imported item (or alias) during the module resolution phase. + /// + /// This function verifies that the requested item exists in the source module and + /// that it has the appropriate public visibility to be imported. If validation passes, + /// the item is registered in the importing module's resolution table and global alias registry. + /// + /// # Arguments + /// + /// * `import_aliases` - The global registry tracking alias chains and their canonical roots. + /// * `resolutions` - The global, mutable array mapping each `file_id` to its localized `FileResolutions` table. + /// * `file_id` - The unique identifier of the module that is *performing* the import (the destination). + /// * `ind` - The unique identifier of the source module being imported *from*. + /// * `aliased_identifier` - The specific identifier (and potential alias) being imported from the source. + /// * `use_decl` - The node of the `use` statement. This dictates the visibility of the new import + /// (e.g., `pub use` re-exports the item publicly). + /// + /// # Returns + /// + /// Returns `None` on success. Returns `Some(RichError)` if: + /// * [`Error::DuplicateAlias`]: The target `alias` (or imported name) has already been used in the current module. + /// * [`Error::UnresolvedItem`]: The target `elem` does not exist in the source module (`ind`). + /// * [`Error::PrivateItem`]: The target exists, but its visibility is explicitly `Private`, + fn process_use_item( + import_aliases: &mut AliasRegistry, + resolutions: &mut [FileResolutions], + file_id: usize, + ind: usize, + (elem, alias): &AliasedIdentifier, + use_decl: &parse::UseDecl, + ) -> Option { + let orig_id = (elem.clone(), ind); + + // 1. Determine the local name and ID up front + let local_name = alias.as_ref().unwrap_or(elem); + let local_id = (local_name.clone(), file_id); + + // 2. Check for collisions + if import_aliases.direct_targets.contains_key(&local_id) { + return Some(RichError::new( + Error::DuplicateAlias(local_name.clone()), + *use_decl.span(), + )); + } + + // 3. Find the true root using a single lookup! + // If `orig_id` exists in resolved_roots, it means it's an alias and we get its true root. + // If it returns None, it means `orig_id` is the original item, so it IS the root. + let true_root = import_aliases + .resolved_roots + .get(&orig_id) + .cloned() + .unwrap_or_else(|| orig_id.clone()); + + // 4. Update the registries + if alias.is_some() { + // Only update the chain if the user explicitly used the `as` keyword + import_aliases + .direct_targets + .insert(local_id.clone(), orig_id); + } + + // Always cache the final root for instant O(1) lookups later + import_aliases.resolved_roots.insert(local_id, true_root); + + // 5. Bind the result to the `identifier` variable + let identifier = local_name.clone(); + + // 6. Verify Existence: Does the item exist in the source file? + let Some(resolution) = resolutions[ind].get(elem) else { + return Some(RichError::new( + Error::UnresolvedItem(elem.clone()), + *use_decl.span(), + )); + }; + + // 7. Verify Visibility: Are we allowed to see it? + if matches!(resolution.visibility, parse::Visibility::Private) { + return Some(RichError::new( + Error::PrivateItem(elem.as_inner().to_string()), + *use_decl.span(), + )); + } + + // 8. Register the item in the local module's namespace + resolutions[file_id].insert( + identifier, + Resolution { + visibility: use_decl.visibility().clone(), + }, + ); + + None + } + + fn register_def( + items: &mut Vec, + resolutions: &mut [FileResolutions], + file_id: usize, + item: &parse::Item, + name: Identifier, + vis: &parse::Visibility, + ) -> Option { + let item = match Item::from_parse(item, file_id) { + Ok(item) => item, + Err(err) => return Some(err), + }; + + items.push(item); + resolutions[file_id].insert( + name, + Resolution { + visibility: vis.clone(), + }, + ); + + None + } + + fn build_program(&self, order: &Vec, handler: &mut ErrorCollector) -> Option { + let mut items: Vec = Vec::new(); + let mut resolutions: Vec = vec![BTreeMap::new(); order.len()]; + let mut import_aliases = AliasRegistry::default(); + + for &file_id in order { + let importer_source = self.modules[file_id].source.clone(); + let program_items = self.modules[file_id].parsed_program.items(); + + for elem in program_items { + let mut errors: Vec = Vec::new(); + match elem { + parse::Item::Use(use_decl) => { + let full_path = match resolve_single_import( + importer_source.clone(), + use_decl, + &self.dependency_map, + ) { + Ok(path) => path, + Err(err) => { + handler.push(err); + continue; + } + }; + + /* + let full_path = match get_full_path(&self.libraries, use_decl) { + Ok(path) => path, + Err(err) => { + handler.push(err.with_source(importer_source.clone())); + continue; + } + }; + */ + let source_full_path = SourceName::Real(Arc::from(full_path)); + let ind = self.lookup[&source_full_path]; + + let use_targets = match use_decl.items() { + parse::UseItems::Single(elem) => std::slice::from_ref(elem), + parse::UseItems::List(elems) => elems.as_slice(), + }; + + for target in use_targets { + if let Some(err) = ProjectGraph::process_use_item( + &mut import_aliases, + &mut resolutions, + file_id, + ind, + target, + use_decl, + ) { + errors.push(err) + } + } + } + parse::Item::TypeAlias(alias) => { + if let Some(err) = Self::register_def( + &mut items, + &mut resolutions, + file_id, + elem, + alias.name().clone().into(), + alias.visibility(), + ) { + errors.push(err) + } + } + parse::Item::Function(function) => { + if let Some(err) = Self::register_def( + &mut items, + &mut resolutions, + file_id, + elem, + function.name().clone().into(), + function.visibility(), + ) { + errors.push(err) + } + } + parse::Item::Module => {} + } + handler.update_with_source_enrichment(importer_source.clone(), errors); + } + } + + if handler.has_errors() { + None + } else { + Some(Program { + items: items.into(), + paths: self.paths.clone(), + import_aliases, + resolutions: resolutions.into(), + span: *self.modules[0].parsed_program.as_ref(), + }) + } + } + + pub fn resolve_complication_order( + &self, + handler: &mut ErrorCollector, + ) -> Result, String> { + let mut order = match self.c3_linearize() { + Ok(order) => order, + Err(err) => return Err(err.to_string()), + }; + order.reverse(); + + Ok(self.build_program(&order, handler)) + } +} + +/// Resolves a single `use` declaration into a physical file path. +fn resolve_single_import( + importer_source: SourceFile, + use_decl: &parse::UseDecl, + dependency_map: &DependencyMap, +) -> Result { + // TODO: @LesterEvSe or someone else, reconsider this architectural approach. + // Consider removing this `match` statement, or dropping `SourceName` from `paths` and `lookup`. + let curr_path = match importer_source.name() { + SourceName::Real(path) => path, + SourceName::Virtual(name) => { + // Notice we use `return Err(...)` here instead of `continue` + return Err(RichError::new( + Error::Resolution(format!( + "Virtual source '{name}' cannot be used to resolve library imports" + )), + *use_decl.span(), + )); + } + }; + + match dependency_map.resolve_path(&curr_path, use_decl) { + Ok(path) => Ok(path), + Err(err) => Err(err.with_source(importer_source.clone())), + } +} + +/// C3 Merge Algorithm +/// +/// Merges a list of sequences (parent linearizations) into a single sequence. +/// The algorithm ensures that the local precedence order of each sequence is preserved. +// Change the return type to Result +fn merge(mut seqs: Vec>) -> Result, Vec>> { + let mut result = Vec::new(); + + loop { + seqs.retain(|s| !s.is_empty()); + if seqs.is_empty() { + return Ok(result); + } + + let mut candidate = None; + + 'outer: for seq in &seqs { + let head = seq[0]; + + if seqs.iter().all(|s| !s[1..].contains(&head)) { + candidate = Some(head); + break 'outer; + } + } + + let Some(head) = candidate else { + return Err(seqs); + }; + + result.push(head); + + for seq in &mut seqs { + if seq.first() == Some(&head) { + seq.remove(0); + } + } + } +} + +impl fmt::Display for Program { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // 1. Print the actual program code first + for item in self.items.iter() { + writeln!(f, "{item}")?; + } + + // 2. Open the Resolution Table block + writeln!(f, "\n/* --- RESOLUTION TABLE ---")?; + + // 3. Logic: Empty vs Populated + if self.resolutions.is_empty() { + writeln!(f, " EMPTY")?; + } else { + for (file_id, scope) in self.resolutions.iter().enumerate() { + if scope.is_empty() { + writeln!(f, " File ID {}: (No resolutions)", file_id)?; + continue; + } + + writeln!(f, " File ID {}:", file_id)?; + + for (ident, resolution) in scope { + writeln!(f, " {}: {:?}", ident, resolution.visibility)?; + } + } + } + + // 4. Close the block (This runs for both empty and non-empty cases) + writeln!(f, "*/")?; + + Ok(()) + } +} + +impl fmt::Display for Item { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TypeAlias(alias) => write!(f, "{alias}"), + Self::Function(function) => write!(f, "{function}"), + // The parse tree contains no information about the contents of modules. + // We print a random empty module `mod witness {}` here + // so that `from_string(to_string(x)) = x` holds for all trees `x`. + Self::Module => write!(f, "mod witness {{}}"), + } + } +} + +impl fmt::Display for TypeAlias { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "type {} = {};", self.name(), self.ty()) + } +} + +impl fmt::Display for Function { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "fn {} [file_id: {}] (", self.name(), self.file_id())?; + for (i, param) in self.params().iter().enumerate() { + if 0 < i { + write!(f, ", ")?; + } + write!(f, "{param}")?; + } + write!(f, ")")?; + if let Some(ty) = self.ret() { + write!(f, " -> {ty}")?; + } + write!(f, " {}", self.body()) + } +} + +impl AsRef for Program { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for Function { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for TypeAlias { + fn as_ref(&self) -> &Span { + &self.span + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for Function { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + ::arbitrary_rec(u, 3) + } +} + +#[cfg(feature = "arbitrary")] +impl crate::ArbitraryRec for Function { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + + let file_id = u.int_in_range(0..=5)?; + let name = FunctionName::arbitrary(u)?; + let len = u.int_in_range(0..=3)?; + let params = (0..len) + .map(|_| parse::FunctionParam::arbitrary(u)) + .collect::>>()?; + let ret = Option::::arbitrary(u)?; + let body = + parse::Expression::arbitrary_rec(u, budget).map(parse::Expression::into_block)?; + Ok(Self { + file_id, + name, + params, + ret, + body, + span: Span::DUMMY, + }) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use std::fs::{self, File}; + use std::io::Write; + use std::path::Path; + use tempfile::TempDir; + + // --- Helper to setup environment --- + // Creates a file with specific content in the temp directory + pub(crate) fn create_simf_file(dir: &Path, rel_path: &str, content: &str) -> PathBuf { + let full_path = dir.join(rel_path); + + // Ensure parent directories exist + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).unwrap(); + } + + let mut file = File::create(&full_path).expect("Failed to create file"); + file.write_all(content.as_bytes()) + .expect("Failed to write content"); + full_path + } + + // Helper to mock the initial root program parsing + fn parse_root(path: &Path) -> (parse::Program, SourceFile) { + // 1. Read file + let content = std::fs::read_to_string(path).expect("Failed to read root file for parsing"); + + // 2. Create SourceFile (needed for the new parser signature) + // Note: We use the full path here; the logic inside `new` handles extension removal if needed + let source = SourceFile::new( + SourceName::Real(Arc::from(path)), + Arc::from(content.clone()), + ); + + // 3. Create a temporary handler just for this parse + let mut handler = ErrorCollector::new(); + + // 4. Parse + let program = + parse::Program::parse_from_str_with_errors(&content, source.clone(), &mut handler); + + // 5. Check results + assert!( + !handler.has_errors(), + "Test Setup Failed: Root file syntax error: {}", + ErrorCollector::to_string(&handler) + ); + + (program.expect("Root parsing failed internally"), source) + } + + /// Bootstraps a mock file system and attempts to construct a `ProjectGraph`. + /// + /// This is the low-level, non-panicking test helper. It is designed specifically for + /// "negative tests" where you expect the graph construction to fail (e.g., due to syntax + /// errors in an imported dependency). + /// + /// The mock environment automatically maps the alias `"lib"` to the `"libs/lib"` directory. + /// + /// # Arguments + /// * `files` - A vector of tuples containing `(file_path, file_content)`. + /// **Note:** One of the files *must* be named exactly `"main.simf"`. + /// + /// # Returns + /// A tuple containing: + /// 1. `Option` - `Some` if construction succeeded, `None` if compilation failed. + /// 2. `ErrorCollector` - Contains all diagnostics emitted during parsing and resolution. + /// 3. `TempDir` - The temporary directory. It must be kept alive until the test completes. + /// + /// # Panics + /// Panics if the `files` vector does not contain a `"main.simf"` entry, or if writing + /// the mock files to the OS filesystem fails. + fn setup_graph_raw( + files: Vec<(&str, &str)>, + ) -> (Option, ErrorCollector, TempDir) { + let temp_dir = TempDir::new().unwrap(); + + // 1. Create Files + let mut root_path = None; + for (name, content) in files { + let path = create_simf_file(temp_dir.path(), name, content); + if name == "main.simf" { + root_path = Some(path); + } + } + let root_p = root_path.expect("Tests must define 'main.simf'"); + + // 2. Setup Libraries (Hardcoded "lib" -> "libs/lib" for simplicity in tests) + let mut dependency_map = DependencyMap::new(); + dependency_map.test_insert_without_canonicalize( + temp_dir.path(), // The root of mock project + "lib".to_string(), + &temp_dir.path().join("libs/lib"), + ); + + // 3. Parse & Build + let (root_program, source) = parse_root(&root_p); + let mut handler = ErrorCollector::new(); + + let result = ProjectGraph::new( + source, + Arc::from(dependency_map), + &root_program, + &mut handler, + ); + + // Return the raw result and the handler so the test can inspect the errors + (result, handler, temp_dir) + } + + /// Bootstraps a mock file system and constructs a valid `ProjectGraph`. + /// + /// This is the standard test helper for "happy path" scenarios. It wraps [`setup_graph_raw`] + /// and mathematically guarantees that the graph construction succeeds. It also generates a + /// convenient filename-to-ID lookup map to make asserting on specific files easier. + /// + /// # Arguments + /// * `files` - A vector of tuples containing `(file_path, file_content)`. + /// **Note:** One of the files *must* be named exactly `"main.simf"`. + /// + /// # Returns + /// A tuple containing: + /// 1. `ProjectGraph` - The fully constructed, valid dependency graph. + /// 2. `HashMap` - A mapping of simple filenames (e.g., `"math.simf"`) to their node IDs. + /// 3. `TempDir` - The temporary directory. It must be kept alive until the test completes. + /// + /// # Panics + /// Panics if the compiler encounters any errors during parsing or resolution, + /// or if `"main.simf"` is missing. For testing compiler errors, use [`setup_graph_raw`] instead. + fn setup_graph(files: Vec<(&str, &str)>) -> (ProjectGraph, HashMap, TempDir) { + let (graph_result, _handler, temp_dir) = setup_graph_raw(files); + + let graph = graph_result.expect( + "setup_graph expects a valid graph construction. Use manual setup for error tests.", + ); + + // Create Lookup (File Name -> ID) for easier asserting + let mut file_ids = HashMap::new(); + for (source_name, id) in &graph.lookup { + let simple_name = match source_name { + SourceName::Real(path) => path.file_name().unwrap().to_string_lossy().to_string(), + SourceName::Virtual(name) => name.to_string(), + }; + file_ids.insert(simple_name, *id); + } + + (graph, file_ids, temp_dir) + } + + #[test] + fn test_local_definitions_visibility() { + // Scenario: + // main.simf defines a private function and a public function. + // Expected: Both should appear in the scope with correct visibility. + + let (graph, ids, _dir) = setup_graph(vec![( + "main.simf", + "fn private_fn() {} pub fn public_fn() {}", + )]); + + let root_id = *ids.get("main").unwrap(); + let order = vec![root_id]; // Only one file + + let mut error_handler = ErrorCollector::new(); + let program = graph + .build_program(&order, &mut error_handler) + .expect("Failed to build program"); + let scope = &program.resolutions[root_id]; + + // Check private function + let private_res = scope + .get(&Identifier::from("private_fn")) + .expect("private_fn missing"); + assert_eq!(private_res.visibility, Visibility::Private); + + // Check public function + let public_res = scope + .get(&Identifier::from("public_fn")) + .expect("public_fn missing"); + assert_eq!(public_res.visibility, Visibility::Public); + } + + #[test] + fn test_pub_use_propagation() { + // Scenario: Re-exporting. + // 1. A.simf defines `pub fn foo`. + // 2. B.simf imports it and re-exports it via `pub use`. + // 3. main.simf imports it from B. + // Expected: B's scope must contain `foo` marked as Public. + + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "pub use lib::A::foo;"), + ("main.simf", "use lib::B::foo;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + + // Manual topological order: A -> B -> Root + let order = vec![id_a, id_b, id_root]; + + let mut error_handler = ErrorCollector::new(); + let program = graph + .build_program(&order, &mut error_handler) + .expect("Failed to build program"); + + // Check B's scope + let scope_b = &program.resolutions[id_b]; + let foo_in_b = scope_b + .get(&Identifier::from("foo")) + .expect("foo missing in B"); + + // This is the critical check: Did `pub use` make it Public in B? + assert_eq!( + foo_in_b.visibility, + Visibility::Public, + "B should re-export foo as Public" + ); + + // Check Root's scope + let scope_root = &program.resolutions[id_root]; + let foo_in_root = scope_root + .get(&Identifier::from("foo")) + .expect("foo missing in Root"); + + // Root imported it via `use` (not pub use), so it should be Private in Root + assert_eq!( + foo_in_root.visibility, + Visibility::Private, + "Root should have foo as Private" + ); + } + + #[test] + fn test_private_import_encapsulation_error() { + // Scenario: Access violation. + // 1. A.simf defines `pub fn foo`. + // 2. B.simf imports it via `use` (Private import). + // 3. main.simf tries to import `foo` from B. + // Expected: Error, because B did not re-export foo. + + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("libs/lib/B.simf", "use lib::A::foo;"), // <--- Private binding! + ("main.simf", "use lib::B::foo;"), // <--- Should fail + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + + // Order: A -> B -> Root + let order = vec![id_a, id_b, id_root]; + + let mut error_handler = ErrorCollector::new(); + let result = graph.build_program(&order, &mut error_handler); + + assert!( + result.is_none(), + "Build should fail when importing a private binding" + ); + + assert!( + error_handler.has_errors(), + "Error handler should contain errors" + ); + + let err_msg = ErrorCollector::to_string(&error_handler); + assert!( + err_msg.contains("private"), + "Error message should mention 'private', but got: \n{}", + err_msg + ); + } + + #[test] + fn test_simple_import() { + // Setup: + // main.simf -> "use lib::math;" + // libs/lib/math.simf -> "" + // Note: Changed "std" to "lib" to match setup_graph default config + + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::math::some_func;"), + ("libs/lib/math.simf", ""), + ]); + + assert_eq!(graph.modules.len(), 2, "Should have Root and Math module"); + + // Check dependency: Root depends on Math + let root_id = ids["main"]; + let math_id = ids["math"]; + + assert!( + graph.dependencies[&root_id].contains(&math_id), + "Root (main.simf) should depend on Math (math.simf)" + ); + } + + #[test] + fn test_c3_simple_import() { + // Setup similar to above + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::math::some_func;"), + ("libs/lib/math.simf", ""), + ]); + + let order = graph.c3_linearize().expect("C3 failed"); + + let root_id = ids["main"]; + let math_id = ids["math"]; + + // Assuming linearization order: Dependent (Root) -> Dependency (Math) + // Or vice-versa based on your specific C3 impl. + // Based on your previous test `vec![0, 1]`, it seems like [Root, Math]. + assert_eq!(order, vec![root_id, math_id]); + } + + #[test] + fn test_diamond_dependency_deduplication() { + // Setup: + // root -> imports A, B + // A -> imports Common + // B -> imports Common + // Expected: Common loaded ONLY ONCE (total 4 modules). + + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::foo; use lib::B::bar;"), + ("libs/lib/A.simf", "use lib::Common::dummy1;"), + ("libs/lib/B.simf", "use lib::Common::dummy2;"), + ("libs/lib/Common.simf", ""), + ]); + + // 1. Check strict deduplication (Unique modules count) + assert_eq!( + graph.modules.len(), + 4, + "Should resolve exactly 4 unique modules (Main, A, B, Common)" + ); + + // 2. Verify Graph Topology via IDs + let a_id = ids["A"]; + let b_id = ids["B"]; + let common_id = ids["Common"]; + + // Check A -> Common + assert!( + graph.dependencies[&a_id].contains(&common_id), + "A should depend on Common" + ); + + // Check B -> Common (Crucial: Must be the SAME common_id) + assert!( + graph.dependencies[&b_id].contains(&common_id), + "B should depend on Common" + ); + } + + #[test] + fn test_c3_diamond_dependency_deduplication() { + // Setup: + // root (main) -> imports A, B + // A -> imports Common + // B -> imports Common + // Expected: Common loaded ONLY ONCE. + + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::foo; use lib::B::bar;"), + ("libs/lib/A.simf", "use lib::Common::dummy1;"), + ("libs/lib/B.simf", "use lib::Common::dummy2;"), + ("libs/lib/Common.simf", ""), + ]); + + let order = graph.c3_linearize().expect("C3 failed"); + + // Verify order using IDs from the helper map + let main_id = ids["main"]; + let a_id = ids["A"]; + let b_id = ids["B"]; + let common_id = ids["Common"]; + + // Common must be first (or early), Main last. + // Exact topological sort might vary for A and B, but Common must be before them. + assert_eq!(order, vec![main_id, a_id, b_id, common_id]); // Or [common, a, b, main] + } + + #[test] + fn test_cyclic_dependency_graph_structure() { + // Setup: A <-> B cycle + // main -> imports A + // A -> imports B + // B -> imports A + + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::entry;"), + ("libs/lib/A.simf", "use lib::B::func;"), + ("libs/lib/B.simf", "use lib::A::func;"), + ]); + + let a_id = ids["A"]; + let b_id = ids["B"]; + + // Check if graph correctly recorded the cycle + assert!( + graph.dependencies[&a_id].contains(&b_id), + "A should depend on B" + ); + assert!( + graph.dependencies[&b_id].contains(&a_id), + "B should depend on A" + ); + } + + #[test] + fn test_c3_detects_cycle() { + // Uses the same logic as above but verifies linearization fails + let (graph, _, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::entry;"), + ("libs/lib/A.simf", "use lib::B::func;"), + ("libs/lib/B.simf", "use lib::A::func;"), + ]); + + let result = graph.c3_linearize(); + assert!(matches!(result, Err(C3Error::CycleDetected(_)))); + } + + #[test] + fn test_ignores_unmapped_imports() { + // Setup: root imports from "unknown", which is not in our lib_map + let (graph, ids, _dir) = setup_graph(vec![("main.simf", "use unknown::library;")]); + + assert_eq!(graph.modules.len(), 1, "Should only contain root"); + assert!(graph.dependencies[&ids["main"]].is_empty()); + } + + // Tests for aliases + #[test] + fn test_renaming_with_use() { + // Scenario: Renaming imports. + // main.simf: use lib::A::foo as bar; + // Expected: Scope should contain "bar", but not "foo". + + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "pub fn foo() {}"), + ("main.simf", "use lib::A::foo as bar;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_root]; + + let mut error_handler = ErrorCollector::new(); + let program = graph + .build_program(&order, &mut error_handler) + .expect("Failed to build program"); + let scope = &program.resolutions[id_root]; + + assert!( + scope.get(&Identifier::from("foo")).is_none(), + "Original name 'foo' should not be in scope" + ); + assert!( + scope.get(&Identifier::from("bar")).is_some(), + "Alias 'bar' should be in scope" + ); + } + + #[test] + fn test_multiple_aliases_in_list() { + // Scenario: Renaming multiple imports inside brackets. + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "pub fn foo() {} pub fn baz() {}"), + ("main.simf", "use lib::A::{foo as bar, baz as qux};"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_root]; + + let mut error_handler = ErrorCollector::new(); + let program = graph + .build_program(&order, &mut error_handler) + .expect("Failed to build program"); + let scope = &program.resolutions[id_root]; + + // The original names should NOT be in scope + assert!(scope.get(&Identifier::from("foo")).is_none()); + assert!(scope.get(&Identifier::from("baz")).is_none()); + + // The aliases MUST be in scope + assert!(scope.get(&Identifier::from("bar")).is_some()); + assert!(scope.get(&Identifier::from("qux")).is_some()); + } + + #[test] + fn test_alias_private_item_fails() { + // Scenario: Attempting to alias a private item should fail. + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "fn secret() {}"), // Note: Missing `pub` + ("main.simf", "use lib::A::secret as my_secret;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_root]; + + let mut error_handler = ErrorCollector::new(); + // This should NOT panic, but it should populate the error handler + graph.build_program(&order, &mut error_handler); + + assert!( + error_handler.has_errors(), + "Compiler should emit an error when aliasing a private item" + ); + + let error_msg = ErrorCollector::to_string(&error_handler); + assert!( + error_msg.contains("PrivateItem") || error_msg.contains("secret"), + "Error should mention the private item restriction" + ); + } + + #[test] + fn test_deep_reexport_with_aliases() { + // Scenario: Chaining aliases across multiple files. + // A.simf: pub fn original() {} + // B.simf: pub use lib::A::original as middle; + // main.simf: use lib::B::middle as final; + + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "pub fn original() {}"), + ("libs/lib/B.simf", "pub use lib::A::original as middle;"), + ("main.simf", "use lib::B::middle as final_name;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + // Crucial: The compiler must process A, then B, then Main! + let order = vec![id_a, id_b, id_root]; + + let mut error_handler = ErrorCollector::new(); + let program = graph + .build_program(&order, &mut error_handler) + .expect("Failed to build program"); + + // Assert Main Scope + let main_scope = &program.resolutions[id_root]; + assert!(main_scope.get(&Identifier::from("original")).is_none()); + assert!(main_scope.get(&Identifier::from("middle")).is_none()); + assert!( + main_scope.get(&Identifier::from("final_name")).is_some(), + "Main must see the final alias" + ); + + // Assert B Scope (It should have the intermediate alias!) + let b_scope = &program.resolutions[id_b]; + assert!( + b_scope.get(&Identifier::from("middle")).is_some(), + "File B must contain its own public alias" + ); + } + + #[test] + fn test_deep_reexport_private_link_fails() { + // Scenario: Main tries to import an alias from B, but B's alias is private! + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/A.simf", "pub fn target() {}"), + // Note: Missing `pub` keyword here! This makes `hidden_alias` private to B. + ("libs/lib/B.simf", "use lib::A::target as hidden_alias;"), + ("main.simf", "use lib::B::hidden_alias;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_b, id_root]; + + let mut error_handler = ErrorCollector::new(); + graph.build_program(&order, &mut error_handler); + + assert!( + error_handler.has_errors(), + "Compiler must emit an error when trying to import a private alias from an intermediate module" + ); + + let error_msg = ErrorCollector::to_string(&error_handler); + assert!( + error_msg.contains("PrivateItem") || error_msg.contains("hidden_alias"), + "Error should correctly identify the private intermediate alias" + ); + } + + #[test] + fn test_alias_cycle_detection() { + // Scenario: A malicious or confused user creates an infinite alias loop. + let (graph, ids, _dir) = setup_graph(vec![ + // A imports from B, B imports from A. + ("libs/lib/A.simf", "pub use lib::B::pong as ping;"), + ("libs/lib/B.simf", "pub use lib::A::ping as pong;"), + ("main.simf", "use lib::A::ping;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_a, id_b, id_root]; + + let mut error_handler = ErrorCollector::new(); + graph.build_program(&order, &mut error_handler); + + // Driver should catch this and emit an UnresolvedItem or Cycle error, + // rather than causing a Stack Overflow! + assert!( + error_handler.has_errors(), + "Compiler must catch infinite alias cycles" + ); + } + + // --- C3 Error Display Tests --- + + #[test] + fn display_c3_cycle_detected() { + let cycle = vec![ + "main.simf".to_string(), + "libs/lib/A.simf".to_string(), + "libs/lib/B.simf".to_string(), + "main.simf".to_string(), + ]; + + let error = C3Error::CycleDetected(cycle); + + let expected = r#"Circular dependency detected: "main.simf -> libs/lib/A.simf -> libs/lib/B.simf -> main.simf""#; + + assert_eq!(error.to_string(), expected); + } + + #[test] + fn display_c3_inconsistent_linearization() { + let conflicts = vec![ + vec!["lib/x".to_string(), "lib/y".to_string()], + vec!["lib/y".to_string(), "lib/x".to_string()], + ]; + + let error = C3Error::InconsistentLinearization { + module: "main".to_string(), + conflicts, + }; + + let expected = r#"Inconsistent resolution order for module 'main' +The compiler could not resolve the following conflicting import constraints: + [lib/x, lib/y] + [lib/y, lib/x] +Try reordering your `use` statements to avoid cross-wiring."# + .to_string(); + + assert_eq!(error.to_string(), expected); + } + + // --- Dependent File Error Display Tests --- + #[test] + fn test_missing_file_error() { + let (result, handler, _dir) = + setup_graph_raw(vec![("main.simf", "use lib::ghost::Phantom;")]); + + assert!(result.is_none(), "Graph construction should fail"); + assert!(handler.has_errors()); + + let error_msg = handler.to_string(); + assert!( + error_msg.contains("File not found") || error_msg.contains("ghost.simf"), + "Error message should mention 'ghost.simf' or 'File not found'. Got: {}", + error_msg + ); + } + + #[test] + #[ignore = "TODO(Error_Formatting): The compiler currently strips the .simf extension from file paths during graph construction. This test expects the extension to be preserved."] + fn test_display_error_in_imported_dependency() { + let (result, handler, _dir) = setup_graph_raw(vec![ + ("main.simf", "use lib::math::add;"), + ("libs/lib/math.simf", "pub fn add(a: u32 b: u32) {}"), // NOTE: The comma is missing on purpose. + ]); + + assert!( + result.is_none(), + "Graph construction should fail due to syntax error in dependency" + ); + assert!( + handler.has_errors(), + "Handler should contain the imported module's error" + ); + + let err_msg = ErrorCollector::to_string(&handler); + + assert!( + err_msg.contains("math.simf:1"), + "Error should correctly display the file name math.simf and line number. Got:\n{}", + err_msg + ); + assert!( + err_msg.contains("pub fn add(a: u32 b: u32) {}"), + "Error should print the snippet from the imported file. Got:\n{}", + err_msg + ); + } + + #[test] + #[ignore = "TODO(Error_Formatting): The compiler currently strips the .simf extension from file paths during graph construction. This test expects the extension to be preserved."] + fn test_display_unresolved_item_in_dependency() { + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/B.simf", "pub fn real() {}"), + ("libs/lib/A.simf", "use lib::B::ghost;\npub fn foo() {}"), + ("main.simf", "use lib::A::foo;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_b, id_a, id_root]; + + let mut handler = ErrorCollector::new(); + let _ = graph.build_program(&order, &mut handler); + + let err_msg = ErrorCollector::to_string(&handler); + + assert!( + err_msg.contains("A.simf:1"), + "Error should point to A.simf where the bad import happened. Got:\n{}", + err_msg + ); + assert!( + err_msg.contains("use lib::B::ghost;"), + "Error should print the snippet from A.simf" + ); + assert!( + err_msg.contains("Unknown item `ghost`"), + "Error should correctly identify the missing item" + ); + } + + #[test] + #[ignore = "TODO(Error_Formatting): The compiler currently strips the .simf extension from file paths during graph construction. This test expects the extension to be preserved."] + fn test_display_private_item_access_in_dependency() { + let (graph, ids, _dir) = setup_graph(vec![ + ("libs/lib/B.simf", "fn secret() {}"), + ("libs/lib/A.simf", "use lib::B::secret;\npub fn foo() {}"), + ("main.simf", "use lib::A::foo;"), + ]); + + let id_a = *ids.get("A").unwrap(); + let id_b = *ids.get("B").unwrap(); + let id_root = *ids.get("main").unwrap(); + let order = vec![id_b, id_a, id_root]; + + let mut handler = ErrorCollector::new(); + let _ = graph.build_program(&order, &mut handler); + + let err_msg = ErrorCollector::to_string(&handler); + + assert!( + err_msg.contains("A.simf:1"), + "Error should point to A.simf where the privacy violation happened. Got:\n{}", + err_msg + ); + assert!( + err_msg.contains("use lib::B::secret;"), + "Error should print the snippet from A.simf" + ); + assert!( + err_msg.contains("Item `secret` is private"), + "Error should correctly identify the privacy violation" + ); + } +} diff --git a/src/error.rs b/src/error.rs index 1ded6fdd..97a01529 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ use std::fmt; use std::ops::Range; -use std::sync::Arc; +use std::path::PathBuf; use chumsky::error::Error as ChumskyError; use chumsky::input::ValueInput; @@ -15,6 +15,7 @@ use crate::lexer::Token; use crate::parse::MatchPattern; use crate::str::{AliasName, FunctionName, Identifier, JetName, ModuleName, WitnessName}; use crate::types::{ResolvedType, UIntType}; +use crate::SourceFile; /// Area that an object spans inside a file. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -117,16 +118,16 @@ impl> WithSpan for Result { } /// Helper trait to update `Result` with the affected source file. -pub trait WithFile { +pub trait WithSource { /// Update the result with the affected source file. /// /// Enable pretty errors. - fn with_file>>(self, file: F) -> Result; + fn with_source>(self, source: S) -> Result; } -impl WithFile for Result { - fn with_file>>(self, file: F) -> Result { - self.map_err(|e| e.with_file(file.into())) +impl WithSource for Result { + fn with_source>(self, source: S) -> Result { + self.map_err(|e| e.with_source(source.into())) } } @@ -136,33 +137,33 @@ impl WithFile for Result { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RichError { /// The error that occurred. - error: Error, + error: Box, /// Area that the error spans inside the file. span: Span, - /// File in which the error occurred. + /// File context in which the error occurred. /// /// Required to print pretty errors. - file: Option>, + source: Option, } impl RichError { /// Create a new error with context. pub fn new(error: Error, span: Span) -> RichError { RichError { - error, + error: Box::new(error), span, - file: None, + source: None, } } /// Add the source file where the error occurred. /// /// Enable pretty errors. - pub fn with_file(self, file: Arc) -> Self { + pub fn with_source(self, source: SourceFile) -> Self { Self { error: self.error, span: self.span, - file: Some(file), + source: Some(source), } } @@ -170,14 +171,14 @@ impl RichError { /// a problem on the parsing side. pub fn parsing_error(reason: &str) -> Self { Self { - error: Error::CannotParse(reason.to_string()), + error: Box::new(Error::CannotParse(reason.to_string())), span: Span::new(0, 0), - file: None, + source: None, } } - pub fn file(&self) -> &Option> { - &self.file + pub fn source(&self) -> &Option { + &self.source } pub fn error(&self) -> &Error { @@ -208,8 +209,10 @@ impl fmt::Display for RichError { (line, col) } - match self.file { - Some(ref file) if !file.is_empty() => { + match self.source { + Some(ref source) if !source.content.is_empty() => { + let file = &source.content(); + let (start_line, start_col) = get_line_col(file, self.span.start); let (end_line, end_col) = get_line_col(file, self.span.end); @@ -218,6 +221,16 @@ impl fmt::Display for RichError { let n_spanned_lines = end_line - start_line_index; let line_num_width = end_line.to_string().len(); + writeln!( + f, + "{:>width$}--> {}:{}:{}", + "", + source.name, + start_line, + start_col, + width = line_num_width + )?; + writeln!(f, "{:width$} |", " ", width = line_num_width)?; let mut lines = file.lines().skip(start_line_index).peekable(); @@ -250,7 +263,7 @@ impl std::error::Error for RichError {} impl From for Error { fn from(error: RichError) -> Self { - error.error + *error.error } } @@ -266,7 +279,7 @@ where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { fn merge(self, other: Self) -> Self { - match (&self.error, &other.error) { + match (&*self.error, &*other.error) { (Error::Grammar(_), Error::Grammar(_)) => other, (Error::Grammar(_), _) => other, (_, Error::Grammar(_)) => self, @@ -302,13 +315,13 @@ where let found_string = found.map(|t| t.to_string()); Self { - error: Error::Syntax { + error: Box::new(Error::Syntax { expected: expected_tokens, label: None, found: found_string, - }, + }), span, - file: None, + source: None, } } } @@ -329,20 +342,20 @@ where let found_string = found.map(|t| t.to_string()); Self { - error: Error::Syntax { + error: Box::new(Error::Syntax { expected: expected_strings, label: None, found: found_string, - }, + }), span, - file: None, + source: None, } } fn label_with(&mut self, label: &'tokens str) { if let Error::Syntax { label: ref mut l, .. - } = &mut self.error + } = &mut *self.error { *l = Some(label.to_string()); } @@ -351,26 +364,36 @@ where #[derive(Debug, Clone, Hash)] pub struct ErrorCollector { - /// File in which the error occurred. - file: Arc, - /// Collected errors. errors: Vec, } +impl Default for ErrorCollector { + fn default() -> Self { + Self::new() + } +} + impl ErrorCollector { - pub fn new(file: Arc) -> Self { - Self { - file, - errors: Vec::new(), - } + pub fn new() -> Self { + Self { errors: Vec::new() } + } + + /// Extend existing errors with concrete `RichError`. + /// We assume that `RichError` contains `SourceFile`. + pub fn push(&mut self, error: RichError) { + self.errors.push(error); } - /// Extend existing errors with slice of new errors. - pub fn update(&mut self, errors: impl IntoIterator) { + /// Extend existing errors with slice of new errors and enrich them with source. + pub fn update_with_source_enrichment( + &mut self, + source: SourceFile, + errors: impl IntoIterator, + ) { let new_errors = errors .into_iter() - .map(|err| err.with_file(Arc::clone(&self.file))); + .map(|err| err.with_source(source.clone())); self.errors.extend(new_errors); } @@ -379,8 +402,8 @@ impl ErrorCollector { &self.errors } - pub fn is_empty(&self) -> bool { - self.get().is_empty() + pub fn has_errors(&self) -> bool { + !self.errors.is_empty() } } @@ -398,10 +421,13 @@ impl fmt::Display for ErrorCollector { /// Records _what_ happened but not where. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Error { + Internal(String), + UnknownLibrary(String), ArraySizeNonZero(usize), ListBoundPow2(usize), BitStringPow2(usize), CannotParse(String), + Resolution(String), Grammar(String), Syntax { expected: Vec, @@ -415,6 +441,9 @@ pub enum Error { CannotCompile(String), JetDoesNotExist(JetName), InvalidCast(ResolvedType, ResolvedType), + FileNotFound(PathBuf), + UnresolvedItem(Identifier), + PrivateItem(String), MainNoInputs, MainNoOutput, MainRequired, @@ -430,6 +459,7 @@ pub enum Error { UndefinedVariable(Identifier), RedefinedAlias(AliasName), UndefinedAlias(AliasName), + DuplicateAlias(Identifier), VariableReuseInPattern(Identifier), WitnessReused(WitnessName), WitnessTypeMismatch(WitnessName, ResolvedType, ResolvedType), @@ -444,6 +474,14 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::Internal(err) => write!( + f, + "INTERNAL ERROR: {err}" + ), + Error::UnknownLibrary(name) => write!( + f, + "Unknown module or library '{name}'" + ), Error::ArraySizeNonZero(size) => write!( f, "Expected a non-negative integer as array size, found {size}" @@ -460,6 +498,10 @@ impl fmt::Display for Error { f, "Cannot parse: {description}" ), + Error::Resolution(description) => write!( + f, + "Resolution error: {description}" + ), Error::Grammar(description) => write!( f, "Grammar error: {description}" @@ -495,6 +537,10 @@ impl fmt::Display for Error { f, "Cannot cast values of type `{source}` as values of type `{target}`" ), + Error::FileNotFound(path) => write!( + f, + "File `{}` not found", path.to_string_lossy() + ), Error::MainNoInputs => write!( f, "Main function takes no input parameters" @@ -515,6 +561,14 @@ impl fmt::Display for Error { f, "Function `{name}` was called but not defined" ), + Error::UnresolvedItem(identifier) => write!( + f, + "Unknown item `{identifier}`" + ), + Error::PrivateItem(name) => write!( + f, + "Item `{name}` is private" + ), Error::InvalidNumberOfArguments(expected, found) => write!( f, "Expected {expected} arguments, found {found} arguments" @@ -555,6 +609,10 @@ impl fmt::Display for Error { f, "Type alias `{identifier}` is not defined" ), + Error::DuplicateAlias(identifier) => write!( + f, + "The alias `{identifier}` was defined multiple times" + ), Error::VariableReuseInPattern(identifier) => write!( f, "Variable `{identifier}` is used twice in the pattern" @@ -586,7 +644,7 @@ impl fmt::Display for Error { Error::ArgumentTypeMismatch(name, declared, assigned) => write!( f, "Parameter `{name}` was declared with type `{declared}` but its assigned argument is of type `{assigned}`" - ), + ) } } } @@ -626,6 +684,10 @@ impl From for Error { #[cfg(test)] mod tests { + use std::sync::Arc; + + use crate::SourceName; + use super::*; const FILE: &str = r#"let a1: List = None; @@ -633,47 +695,61 @@ let x: u32 = Left( Right(0) );"#; const EMPTY_FILE: &str = ""; + const FILENAME: &str = ""; #[test] fn display_single_line() { + let source = SourceFile::new(SourceName::Virtual(Arc::from(FILENAME)), Arc::from(FILE)); + let error = Error::ListBoundPow2(5) .with_span(Span::new(13, 19)) - .with_file(Arc::from(FILE)); - let expected = r#" + .with_source(source); + let expected = format!( + r#" + --> {FILENAME}:1:14 | 1 | let a1: List = None; - | ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"#; + | ^^^^^^ Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found 5"# + ); assert_eq!(&expected[1..], &error.to_string()); } #[test] fn display_multi_line() { + let source = SourceFile::new(SourceName::Virtual(Arc::from(FILENAME)), Arc::from(FILE)); let error = Error::CannotParse( "Expected value of type `u32`, got `Either, _>`".to_string(), ) .with_span(Span::new(41, FILE.len())) - .with_file(Arc::from(FILE)); - let expected = r#" + .with_source(source); + let expected = format!( + r#" + --> {FILENAME}:2:14 | 2 | let x: u32 = Left( 3 | Right(0) 4 | ); - | ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either, _>`"#; + | ^^^^^^^^^^^^^^^^^^ Cannot parse: Expected value of type `u32`, got `Either, _>`"# + ); assert_eq!(&expected[1..], &error.to_string()); } #[test] fn display_entire_file() { + let source = SourceFile::new(SourceName::Virtual(Arc::from(FILENAME)), Arc::from(FILE)); let error = Error::CannotParse("This span covers the entire file".to_string()) .with_span(Span::from(FILE)) - .with_file(Arc::from(FILE)); - let expected = r#" + .with_source(source); + let expected = format!( + r#" + --> {FILENAME}:1:1 | 1 | let a1: List = None; 2 | let x: u32 = Left( 3 | Right(0) 4 | ); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"#; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot parse: This span covers the entire file"# + ); assert_eq!(&expected[1..], &error.to_string()); } @@ -691,10 +767,80 @@ let x: u32 = Left( #[test] fn display_empty_file() { + let source = SourceFile::new( + SourceName::Virtual(Arc::from(FILENAME)), + Arc::from(EMPTY_FILE), + ); let error = Error::CannotParse("This error has an empty file".to_string()) .with_span(Span::from(EMPTY_FILE)) - .with_file(Arc::from(EMPTY_FILE)); + .with_source(source); let expected = "Cannot parse: This error has an empty file"; assert_eq!(&expected, &error.to_string()); } + + #[test] + fn display_error_inside_imported_module() { + let module_name = "libs/math.simf"; + let module_content = "pub fn add(a: u32, b: u32) -> u64 {\n a + b\n}"; + let source = SourceFile::new( + SourceName::Virtual(Arc::from(module_name)), + Arc::from(module_content), + ); + + let error = Error::InvalidNumberOfArguments(2, 1) + .with_span(Span::new(40, 45)) + .with_source(source); + + let expected = r#" + --> libs/math.simf:2:6 + | +2 | a + b + | ^^^^^ Expected 2 arguments, found 1 arguments"# + .to_string(); + assert_eq!(&expected[1..], &error.to_string()); + } + + #[test] + fn display_unresolved_import_in_main_file() { + let main_name = "main.simf"; + let main_content = "use libs::math::multiply;\n\nfn main() {}"; + let source = SourceFile::new( + SourceName::Virtual(Arc::from(main_name)), + Arc::from(main_content), + ); + + let error = Error::UnresolvedItem("multiply".into()) + .with_span(Span::new(16, 24)) + .with_source(source); + + let expected = r#" + --> main.simf:1:17 + | +1 | use libs::math::multiply; + | ^^^^^^^^ Unknown item `multiply`"# + .to_string(); + assert_eq!(&expected[1..], &error.to_string()); + } + + #[test] + fn display_private_item_import_across_modules() { + let main_name = "src/auth_test.simf"; + let main_content = "use libs::auth::SecretToken;\n\nfn main() {}"; + let source = SourceFile::new( + SourceName::Virtual(Arc::from(main_name)), + Arc::from(main_content), + ); + + let error = Error::PrivateItem("SecretToken".into()) + .with_span(Span::new(16, 27)) + .with_source(source); + + let expected = r#" + --> src/auth_test.simf:1:17 + | +1 | use libs::auth::SecretToken; + | ^^^^^^^^^^^ Item `SecretToken` is private"# + .to_string(); + assert_eq!(&expected[1..], &error.to_string()); + } } diff --git a/src/lexer.rs b/src/lexer.rs index 71c004b6..94a6fe78 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -8,9 +8,22 @@ use crate::str::{Binary, Decimal, Hexadecimal}; pub type Spanned = (T, SimpleSpan); pub type Tokens<'src> = Vec<(Token<'src>, crate::error::Span)>; +// Define your SSoT here +// TODO: @LesterEvSe or @Sdoba16 - Migrate to raw identifiers (`r#ident`) for forward compatibility. +// As the language grows, adding new keywords (e.g., `mut`) will break legacy code +// that uses those names for variables. Supporting `r#` escapes prevents this collision. +// BLOCKER: The Imports PR must not be merged until this feature is implemented +pub const RESERVED_TOKENS: &[&str] = &[ + "pub", "use", "as", "fn", "let", "type", "mod", "const", "match", "true", "false", "jet", + "witness", "param", +]; + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Token<'src> { // Keywords + Pub, + Use, + As, Fn, Let, Type, @@ -20,6 +33,10 @@ pub enum Token<'src> { // Control symbols Arrow, + /// Represents a contiguous `::` token. + /// This prevents the lexer from allowing spaces between colons (e.g., `use a: :b`), + /// ensuring we strictly parse valid paths. + DoubleColon, Colon, Semi, Comma, @@ -63,6 +80,9 @@ pub enum Token<'src> { impl<'src> fmt::Display for Token<'src> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Token::Pub => write!(f, "pub"), + Token::Use => write!(f, "use"), + Token::As => write!(f, "as"), Token::Fn => write!(f, "fn"), Token::Let => write!(f, "let"), Token::Type => write!(f, "type"), @@ -71,6 +91,7 @@ impl<'src> fmt::Display for Token<'src> { Token::Match => write!(f, "match"), Token::Arrow => write!(f, "->"), + Token::DoubleColon => write!(f, "::"), Token::Colon => write!(f, ":"), Token::Semi => write!(f, ";"), Token::Comma => write!(f, ","), @@ -134,6 +155,9 @@ pub fn lexer<'src>( choice((just("assert!"), just("panic!"), just("dbg!"), just("list!"))).map(Token::Macro); let keyword = text::ident().map(|s| match s { + "pub" => Token::Pub, + "use" => Token::Use, + "as" => Token::As, "fn" => Token::Fn, "let" => Token::Let, "type" => Token::Type, @@ -162,6 +186,7 @@ pub fn lexer<'src>( just("->").to(Token::Arrow), just("=>").to(Token::FatArrow), just("=").to(Token::Eq), + just("::").to(Token::DoubleColon), // NOTE: It must be before ":", otherwise it does not work just(":").to(Token::Colon), just(";").to(Token::Semi), just(",").to(Token::Comma), diff --git a/src/lib.rs b/src/lib.rs index b4a1032a..ab426c1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod array; pub mod ast; pub mod compile; pub mod debug; +pub mod driver; pub mod dummy_env; pub mod error; pub mod jet; @@ -20,6 +21,8 @@ pub mod types; pub mod value; mod witness; +use std::io; +use std::path::{Path, PathBuf}; use std::sync::Arc; use simplicity::jet::elements::ElementsEnv; @@ -30,19 +33,212 @@ pub extern crate simplicity; pub use simplicity::elements; use crate::debug::DebugSymbols; -use crate::error::{ErrorCollector, WithFile}; -use crate::parse::ParseFromStrWithErrors; +use crate::driver::ProjectGraph; +use crate::error::{Error, ErrorCollector, RichError, WithSource, WithSpan}; +use crate::parse::{ParseFromStrWithErrors, UseDecl}; pub use crate::types::ResolvedType; pub use crate::value::Value; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum SourceName { + Real(Arc), + Virtual(Arc), +} + +impl SourceName { + pub fn without_extension(&self) -> SourceName { + match self { + SourceName::Real(path) => SourceName::Real(Arc::from(path.with_extension(""))), + SourceName::Virtual(name) => SourceName::Virtual(name.clone()), + } + } +} + +impl Default for SourceName { + fn default() -> Self { + SourceName::Virtual(Arc::from("")) + } +} + +use std::fmt; + +impl fmt::Display for SourceName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SourceName::Real(path) => write!(f, "{}", path.display()), + SourceName::Virtual(name) => write!(f, "{}", name), + } + } +} + +/// Represents a source file containing code. +/// +/// Groups the file's name and its content together to guarantee +/// they are always synchronized when present. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct SourceFile { + /// The name or path of the source file (e.g., "main.simf"). + name: SourceName, + /// The actual text content of the source file. + content: Arc, +} + +impl From<(SourceName, &str)> for SourceFile { + fn from((name, content): (SourceName, &str)) -> Self { + Self { + name, + content: Arc::from(content), + } + } +} + +impl SourceFile { + pub fn new(name: SourceName, content: Arc) -> Self { + Self { name, content } + } + + pub fn name(&self) -> SourceName { + self.name.clone() + } + + pub fn content(&self) -> Arc { + self.content.clone() + } +} + +/// A single dependency rule. +#[derive(Debug, Clone)] +pub struct Remapping { + /// The base directory/file that owns this dependency + pub context_prefix: PathBuf, + /// The name used in the `use` statement (e.g., "math") + pub alias: String, + /// The physical path this alias points to + pub target: PathBuf, +} + +/// A list of dependencies, strictly sorted by longest prefix match. +#[derive(Debug, Default)] +pub struct DependencyMap { + inner: Vec, +} + +impl DependencyMap { + pub fn new() -> Self { + Self::default() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Inserts a dependency remapping without interacting with the physical file system. + /// + /// **Warning:** This method completely bypasses OS path canonicalization (`std::fs::canonicalize`). + /// It is designed strictly for unit testing and virtual file environments where the + /// provided paths might not actually exist on the hard drive. + /// + /// Like the standard `insert` method, it internally re-sorts the remapping vector + /// to mathematically guarantee that Longest Prefix Matching is used during path resolution. + pub fn test_insert_without_canonicalize(&mut self, context: &Path, alias: String, path: &Path) { + self.inner.push(Remapping { + context_prefix: context.to_path_buf(), + alias, + target: path.to_path_buf(), + }); + + self.inner.sort_by(|a, b| { + let len_a = a.context_prefix.as_os_str().len(); + let len_b = b.context_prefix.as_os_str().len(); + len_b.cmp(&len_a) + }); + } + + /// Add a dependency mapped to a specific calling file's path prefix. + /// Re-sorts the vector internally to guarantee the Longest Prefix Match. + pub fn insert(&mut self, context: &Path, alias: String, path: &Path) -> io::Result<()> { + let canon_context = std::fs::canonicalize(context).map_err(|err| { + io::Error::new( + err.kind(), + format!( + "Failed to find context directory '{}': {}", + context.display(), + err + ), + ) + })?; + + let canon_path = std::fs::canonicalize(path).map_err(|err| { + io::Error::new( + err.kind(), + format!( + "Failed to find library target path '{}': {}", + path.display(), + err + ), + ) + })?; + + self.inner.push(Remapping { + context_prefix: canon_context, + alias, + target: canon_path, + }); + + // Re-sort the vector so the longest context paths are always at the front! + // This mathematically guarantees that the first match we find is the most specific. + self.inner.sort_by(|a, b| { + let len_a = a.context_prefix.as_os_str().len(); + let len_b = b.context_prefix.as_os_str().len(); + len_b.cmp(&len_a) // Descending order + }); + + Ok(()) + } + + /// Resolve `use alias::...` into a physical file path by finding the + /// most specific library context that owns the current file. + pub fn resolve_path( + &self, + current_file: &Path, + use_decl: &UseDecl, + ) -> Result { + // Safely extract the first segment (the alias) + let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_ref()).collect(); + let first_segment = parts.first().copied().ok_or_else(|| { + Error::CannotParse("Empty use path".to_string()).with_span(*use_decl.span()) + })?; + + // Because the vector is sorted by longest prefix, + // the VERY FIRST match we find is guaranteed to be the correct one! + for remapping in &self.inner { + // Check if the current file is inside the context's directory tree + if !current_file.starts_with(&remapping.context_prefix) { + continue; + } + + // Check if the alias matches what the user typed + if remapping.alias == first_segment { + let mut resolved_path = remapping.target.clone(); + resolved_path.extend(&parts[1..]); + return Ok(resolved_path); + } + } + + println!("Got an error"); + + Err(Error::UnknownLibrary(first_segment.to_string())).with_span(*use_decl.span()) + } +} + /// The template of a SimplicityHL program. /// /// A template has parameterized values that need to be supplied with arguments. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TemplateProgram { simfony: ast::Program, - file: Arc, + source: SourceFile, } impl TemplateProgram { @@ -51,19 +247,47 @@ impl TemplateProgram { /// ## Errors /// /// The string is not a valid SimplicityHL program. - pub fn new>>(s: Str) -> Result { + pub fn new>>( + source_name: SourceName, + dependency_map: Arc, + s: Str, + ) -> Result { + let source_name = source_name.without_extension(); let file = s.into(); - let mut error_handler = ErrorCollector::new(Arc::clone(&file)); - let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler); - if let Some(program) = parse_program { - let ast_program = ast::Program::analyze(&program).with_file(Arc::clone(&file))?; - Ok(Self { - simfony: ast_program, - file, - }) + let source = SourceFile::new(source_name.clone(), file.clone()); + + // Create Global error_handler + let mut error_handler = ErrorCollector::new(); + + // 1. Parse root file + let parsed_program = + parse::Program::parse_from_str_with_errors(&file, source.clone(), &mut error_handler) + .ok_or_else(|| error_handler.to_string())?; + + // 2. Create the driver program + let driver_program: driver::Program = if dependency_map.is_empty() { + driver::Program::from_parse(&parsed_program, source.clone(), &mut error_handler) + .ok_or_else(|| error_handler.to_string())? } else { - Err(ErrorCollector::to_string(&error_handler))? - } + let graph = ProjectGraph::new( + source.clone(), + dependency_map, + &parsed_program, + &mut error_handler, + ) + .ok_or_else(|| error_handler.to_string())?; + + graph + .resolve_complication_order(&mut error_handler)? + .ok_or_else(|| error_handler.to_string())? + }; + + // 3. AST Analysis + let ast_program = ast::Program::analyze(&driver_program).with_source(source.clone())?; + Ok(Self { + simfony: ast_program, + source, + }) } /// Access the parameters of the program. @@ -94,10 +318,10 @@ impl TemplateProgram { let commit = self .simfony .compile(arguments, include_debug_symbols) - .with_file(Arc::clone(&self.file))?; + .with_source(self.source.clone())?; Ok(CompiledProgram { - debug_symbols: self.simfony.debug_symbols(self.file.as_ref()), + debug_symbols: self.simfony.debug_symbols(self.source.content.as_ref()), simplicity: commit, witness_types: self.simfony.witness_types().shallow_clone(), parameter_types: self.simfony.parameters().shallow_clone(), @@ -129,11 +353,13 @@ impl CompiledProgram { /// - [`TemplateProgram::new`] /// - [`TemplateProgram::instantiate`] pub fn new>>( + source_name: SourceName, + dependency_map: Arc, s: Str, arguments: Arguments, include_debug_symbols: bool, ) -> Result { - TemplateProgram::new(s) + TemplateProgram::new(source_name, dependency_map, s) .and_then(|template| template.instantiate(arguments, include_debug_symbols)) } @@ -213,12 +439,20 @@ impl SatisfiedProgram { /// - [`TemplateProgram::instantiate`] /// - [`CompiledProgram::satisfy`] pub fn new>>( + source_name: SourceName, + dependency_map: Arc, s: Str, arguments: Arguments, witness_values: WitnessValues, include_debug_symbols: bool, ) -> Result { - let compiled = CompiledProgram::new(s, arguments, include_debug_symbols)?; + let compiled = CompiledProgram::new( + source_name, + dependency_map, + s, + arguments, + include_debug_symbols, + )?; compiled.satisfy(witness_values) } @@ -317,14 +551,32 @@ pub(crate) mod tests { impl TestCase { pub fn template_file>(program_file_path: P) -> Self { let program_text = std::fs::read_to_string(program_file_path).unwrap(); - Self::template_text(Cow::Owned(program_text)) + Self::template_text( + SourceName::default(), + Arc::from(DependencyMap::new()), + Cow::Owned(program_text), + ) } - pub fn template_text(program_text: Cow) -> Self { - let program = match TemplateProgram::new(program_text.as_ref()) { - Ok(x) => x, - Err(error) => panic!("{error}"), - }; + pub fn template_lib( + source_name: SourceName, + dependency_map: Arc, + program_file: &Path, + ) -> Self { + let program_text = std::fs::read_to_string(program_file).unwrap(); + Self::template_text(source_name, dependency_map, Cow::Owned(program_text)) + } + + pub fn template_text( + source_name: SourceName, + dependency_map: Arc, + program_text: Cow, + ) -> Self { + let program = + match TemplateProgram::new(source_name, dependency_map, program_text.as_ref()) { + Ok(x) => x, + Err(error) => panic!("{error}"), + }; Self { program, lock_time: elements::LockTime::ZERO, @@ -367,8 +619,45 @@ pub(crate) mod tests { } pub fn program_text(program_text: Cow) -> Self { - TestCase::::template_text(program_text) - .with_arguments(Arguments::default()) + TestCase::::template_text( + SourceName::default(), + Arc::from(DependencyMap::new()), + program_text, + ) + .with_arguments(Arguments::default()) + } + + pub fn program_file_with_libs(program_file: P, libs: I) -> Self + where + P: AsRef, + I: IntoIterator, + K: Into, + { + let path_ref = + std::fs::canonicalize(program_file).expect("Failed to canonicalize program path"); + let path_ref = path_ref.as_path(); + + let mut dependency_map = DependencyMap::new(); + for (context, alias, target) in libs { + let context = + std::fs::canonicalize(context).expect("Failed to canonicalize program path"); + let target = + std::fs::canonicalize(target).expect("Failed to canonicalize program path"); + dependency_map + .insert(context.as_ref(), alias.into(), target.as_ref()) + .unwrap(); + } + + let source_name = SourceName::Real(Arc::from( + path_ref.parent().unwrap_or_else(|| Path::new(".")), + )); + + TestCase::::template_lib( + source_name, + Arc::from(dependency_map), + path_ref, + ) + .with_arguments(Arguments::default()) } #[cfg(feature = "serde")] @@ -470,6 +759,214 @@ pub(crate) mod tests { } } + /// THE DEFAULT HELPER + /// Automatically sets up the standard `lib` self-referencing dependency. + fn run_dependency_test(root_path: &str, lib_alias: &str) { + let root_path = PathBuf::from(root_path); + let lib_path = root_path.join(lib_alias); + let main_path = root_path.join("main.simf"); + + TestCase::program_file_with_libs( + &main_path, + [ + (&root_path, lib_alias, &lib_path), + (&lib_path, lib_alias, &lib_path), + ], + ) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + /// THE ADVANCED HELPER + /// A helper function to run standard library dependency tests. + /// `deps` expects an array of tuples: `(context_folder, alias, target_folder)`. + /// Use `"."` for the `context_folder` if the context is the root test directory. + fn run_multi_lib_test(root_path: &str, deps: &[(&str, &str, &str)]) { + let root_path = PathBuf::from(root_path); + let main_path = root_path.join("main.simf"); + + // Convert the string slices into proper PathBufs dynamically + let mapped_deps: Vec<(PathBuf, &str, PathBuf)> = deps + .iter() + .map(|(ctx, alias, target)| { + let ctx_path = if *ctx == "." { + root_path.clone() + } else { + root_path.join(ctx) + }; + + let target_path = root_path.join(target); + + (ctx_path, *alias, target_path) + }) + .collect(); + + let ref_deps = mapped_deps.iter().map(|(c, a, t)| (c, *a, t)); + + TestCase::program_file_with_libs(&main_path, ref_deps) + .with_witness_values(WitnessValues::default()) + .assert_run_success(); + } + + const VALID_TESTS_DIR: &str = "./functional-tests/valid-test-cases"; + const ERROR_TESTS_DIR: &str = "./functional-tests/error-test-cases"; + + // Real test cases + #[test] + fn module_simple() { + run_dependency_test(format!("{}/module-simple", VALID_TESTS_DIR).as_str(), "lib"); + } + + #[test] + fn diamond_dependency_resolution() { + run_dependency_test( + format!("{}/diamond-dependency-resolution", VALID_TESTS_DIR).as_str(), + "lib", + ); + } + + #[test] + fn deep_reexport_chain() { + run_dependency_test( + format!("{}/deep-reexport-chain", VALID_TESTS_DIR).as_str(), + "lib", + ); + } + + #[test] + fn leaky_signature() { + run_dependency_test( + format!("{}/leaky-signature", VALID_TESTS_DIR).as_str(), + "lib", + ); + } + + #[test] + fn reexport_diamond() { + run_dependency_test( + format!("{}/reexport-diamond", VALID_TESTS_DIR).as_str(), + "lib", + ); + } + + // TODO: @Sdoba16, un-ignore this test once teh parser supports r#. + // Please also add a negative test to ensure parsing fails without r#. + #[test] + #[ignore] + // Move this validation into a separate test. + // #[should_panic( + // expected = "Error: The identifier `jet` is a reserved keyword for intrinsic operations and cannot be utilized as a library name." + // )] + fn using_jet_as_module() { + run_dependency_test( + format!("{}/keyword-as-lib", VALID_TESTS_DIR).as_str(), + "jet", + ); + } + + #[test] + fn multi_lib_facade_resolution() { + run_multi_lib_test( + format!("{}/multi-lib-facade", VALID_TESTS_DIR).as_str(), + &[ + (".", "api", "api"), + ("crypto", "math", "math"), + ("api", "crypto", "crypto"), + ("api", "math", "math"), + ], + ); + } + + #[test] + fn interleaved_waterfall() { + run_multi_lib_test( + format!("{}/interleaved-waterfall", VALID_TESTS_DIR).as_str(), + &[ + (".", "orch", "orch"), + ("orch", "db", "db"), + ("orch", "auth", "auth"), + ("orch", "types", "types"), + ("db", "types", "types"), + ("auth", "types", "types"), + ("auth", "db", "db"), + ], + ); + } + + // Error tests + #[test] + #[should_panic(expected = "Inconsistent resolution order")] + fn cross_wire_linearization_error() { + run_dependency_test(format!("{}/cross-wire", ERROR_TESTS_DIR).as_str(), "lib"); + } + + #[test] + #[should_panic(expected = "Item `SecretType` is private")] + fn private_type_visibility_error() { + run_dependency_test( + format!("{}/private-visibility", ERROR_TESTS_DIR).as_str(), + "lib", + ); + } + + #[test] + #[should_panic( + expected = "Cannot parse: found '*' expected '/', jet, witness, param, 'a', 'p', 'd', 'l', identifier, '0', something else, '-', '=', ':', ';', ',', '(', ')', '[', ']', '{', '}', '<', or '>'" + )] + fn global_import_error() { + run_dependency_test(format!("{}/global", ERROR_TESTS_DIR).as_str(), "lib"); + } + + #[test] + #[should_panic(expected = "Circular dependency detected:")] + fn cyclic_dependency_error() { + run_dependency_test( + format!("{}/cyclic-dependency", ERROR_TESTS_DIR).as_str(), + "lib", + ); + } + + #[test] + #[should_panic(expected = "No such file or directory")] + fn file_not_found_error() { + run_dependency_test( + format!("{}/file-not-found", ERROR_TESTS_DIR).as_str(), + "lib", + ); + } + + #[test] + #[should_panic(expected = "No such file or directory")] + fn lib_not_found_error() { + run_dependency_test(format!("{}/lib-not-found", ERROR_TESTS_DIR).as_str(), "lib"); + } + + // Examples + #[test] + fn single_lib() { + run_dependency_test("./examples/single_lib", "temp"); + } + + #[test] + fn simple_multilib() { + run_multi_lib_test( + "./examples/simple_multilib", + &[(".", "math", "math"), (".", "crypto", "crypto")], + ); + } + + #[test] + fn multiple_libs() { + run_multi_lib_test( + "./examples/multiple_libs", + &[ + (".", "merkle", "merkle"), + (".", "base_math", "math"), + ("merkle", "math", "math"), + ], + ); + } + #[test] fn cat() { TestCase::program_file("./examples/cat.simf") @@ -658,6 +1155,8 @@ fn main() { } "#; match SatisfiedProgram::new( + SourceName::default(), + Arc::from(DependencyMap::new()), prog_text, Arguments::default(), WitnessValues::default(), diff --git a/src/main.rs b/src/main.rs index 0be2b086..3165c436 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,8 @@ use base64::display::Base64Display; use base64::engine::general_purpose::STANDARD; use clap::{Arg, ArgAction, Command}; -use simplicityhl::{AbiMeta, CompiledProgram}; -use std::{env, fmt}; +use simplicityhl::{AbiMeta, CompiledProgram, DependencyMap, SourceName}; +use std::{env, fmt, sync::Arc}; #[cfg_attr(feature = "serde", derive(serde::Serialize))] /// The compilation output. @@ -46,6 +46,13 @@ fn main() -> Result<(), Box> { .action(ArgAction::Set) .help("SimplicityHL program file to build"), ) + .arg( + Arg::new("dependencies") + .long("dep") + .value_name("[CONTEXT:]ALIAS=PATH") + .action(ArgAction::Append) + .help("Link a dependency, optionally scoped to a specific module (e.g., --dep ./libs/merkle:math=./libs/math)"), + ) .arg( Arg::new("wit_file") .long("wit") @@ -85,8 +92,11 @@ fn main() -> Result<(), Box> { let matches = command.get_matches(); let prog_file = matches.get_one::("prog_file").unwrap(); - let prog_path = std::path::Path::new(prog_file); - let prog_text = std::fs::read_to_string(prog_path).map_err(|e| e.to_string())?; + let main_path = std::fs::canonicalize(prog_file).unwrap_or_else(|_| { + panic!("Failed to find the program file: '{}'", prog_file); + }); + let main_path = main_path.as_path(); + let prog_text = std::fs::read_to_string(main_path).map_err(|e| e.to_string())?; let include_debug_symbols = matches.get_flag("debug"); let output_json = matches.get_flag("json"); let abi_param = matches.get_flag("abi"); @@ -110,7 +120,50 @@ fn main() -> Result<(), Box> { simplicityhl::Arguments::default() }; - let compiled = match CompiledProgram::new(prog_text, args_opt, include_debug_symbols) { + let lib_args = matches + .get_many::("dependencies") + .unwrap_or_default(); + + let mut libraries = DependencyMap::new(); + + for arg in lib_args { + // Split by '=' to separate the target path + let (left_side, path_str) = arg.split_once('=').unwrap_or_else(|| { + eprintln!( + "Error: Library argument must be in format [CONTEXT:]ALIAS=PATH, got '{arg}'" + ); + std::process::exit(1); + }); + + // Split by ':' to check for a specific context + let (context_path, alias) = if let Some((ctx_str, alias_str)) = left_side.split_once(':') { + // Specific context provided (e.g., merkle:base_math=...) + // We convert it to PathBuf so it shares the same type as main_path + (std::path::Path::new(ctx_str), alias_str) + } else { + // No context provided (e.g., math=...). Bind it to the main file! + (main_path, left_side) + }; + + // Insert and handle the potential io::Error! + // We convert path_str to PathBuf so it maches the tyep of context_path + if let Err(e) = libraries.insert( + context_path, + alias.to_string(), + std::path::Path::new(path_str), + ) { + eprintln!("Error: {e}"); + std::process::exit(1); + } + } + + let compiled = match CompiledProgram::new( + SourceName::Real(Arc::from(main_path)), + Arc::from(libraries), + prog_text, + args_opt, + include_debug_symbols, + ) { Ok(program) => program, Err(e) => { eprintln!("{}", e); diff --git a/src/parse.rs b/src/parse.rs index c42c6f3d..d1d70ec7 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -18,7 +18,6 @@ use miniscript::iter::{Tree, TreeLike}; use crate::error::ErrorCollector; use crate::error::{Error, RichError, Span}; -use crate::impl_eq_hash; use crate::lexer::Token; use crate::num::NonZeroPow2Usize; use crate::pattern::Pattern; @@ -27,6 +26,7 @@ use crate::str::{ WitnessName, }; use crate::types::{AliasedType, BuiltinAlias, TypeConstructible}; +use crate::{impl_eq_hash, SourceFile}; /// A program is a sequence of items. #[derive(Clone, Debug)] @@ -52,13 +52,63 @@ pub enum Item { TypeAlias(TypeAlias), /// A function. Function(Function), + /// Use keyword to load other items + Use(UseDecl), /// A module, which is ignored. Module, } +/// Definition of a declaration +#[derive(Clone, Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct UseDecl { + visibility: Visibility, + path: Vec, + items: UseItems, + span: Span, +} + +impl UseDecl { + /// Access the visibility of the use declaration. + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + /// Access the visibility of the function. + pub fn path(&self) -> &Vec { + &self.path + } + + pub fn path_buf(&self) -> std::path::PathBuf { + self.path().iter().map(|s| s.as_ref()).collect() + } + + /// Access the visibility of the function. + pub fn items(&self) -> &UseItems { + &self.items + } + + /// Access the span of the use declaration. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(UseDecl; visibility, path, items); + +pub type AliasedIdentifier = (Identifier, Option); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum UseItems { + Single(AliasedIdentifier), + List(Vec), +} + /// Definition of a function. #[derive(Clone, Debug)] pub struct Function { + visibility: Visibility, name: FunctionName, params: Arc<[FunctionParam]>, ret: Option, @@ -66,7 +116,19 @@ pub struct Function { span: Span, } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum Visibility { + Public, + Private, +} + impl Function { + /// Access the visibility of the function. + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + /// Access the name of the function. pub fn name(&self) -> &FunctionName { &self.name @@ -95,7 +157,7 @@ impl Function { } } -impl_eq_hash!(Function; name, params, ret, body); +impl_eq_hash!(Function; visibility, name, params, ret, body); /// Parameter of a function. #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -222,12 +284,18 @@ pub enum CallName { #[derive(Clone, Debug)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct TypeAlias { + visibility: Visibility, name: AliasName, ty: AliasedType, span: Span, } impl TypeAlias { + /// Access the visibility of the alias. + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + /// Access the name of the alias. pub fn name(&self) -> &AliasName { &self.name @@ -247,7 +315,7 @@ impl TypeAlias { } } -impl_eq_hash!(TypeAlias; name, ty); +impl_eq_hash!(TypeAlias; visibility, name, ty); /// An expression is something that returns a value. #[derive(Clone, Debug)] @@ -269,7 +337,7 @@ impl Expression { /// Convert the expression into a block expression. #[cfg(feature = "arbitrary")] - fn into_block(self) -> Self { + pub(crate) fn into_block(self) -> Self { match self.inner { ExpressionInner::Single(_) => Expression { span: self.span, @@ -556,6 +624,7 @@ impl fmt::Display for Item { match self { Self::TypeAlias(alias) => write!(f, "{alias}"), Self::Function(function) => write!(f, "{function}"), + Self::Use(use_declaration) => write!(f, "{use_declaration}"), // The parse tree contains no information about the contents of modules. // We print a random empty module `mod witness {}` here // so that `from_string(to_string(x)) = x` holds for all trees `x`. @@ -564,15 +633,30 @@ impl fmt::Display for Item { } } +impl fmt::Display for Visibility { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Private => write!(f, ""), + Self::Public => write!(f, "pub "), + } + } +} + impl fmt::Display for TypeAlias { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "type {} = {};", self.name(), self.ty()) + write!( + f, + "{}type {} = {};", + self.visibility(), + self.name(), + self.ty() + ) } } impl fmt::Display for Function { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "fn {}(", self.name())?; + write!(f, "{}fn {}(", self.visibility(), self.name())?; for (i, param) in self.params().iter().enumerate() { if 0 < i { write!(f, ", ")?; @@ -587,6 +671,56 @@ impl fmt::Display for Function { } } +impl fmt::Display for UseDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let _ = write!(f, "{}use ", self.visibility()); + + for (i, segment) in self.path.iter().enumerate() { + if i > 0 { + write!(f, "::")?; + } + write!(f, "{}", segment)?; + } + + if !self.path.is_empty() { + write!(f, "::")?; + } + + write!(f, "{};", self.items) + } +} + +impl fmt::Display for UseItems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UseItems::Single((ident, alias)) => { + write!(f, "{};", ident)?; + + if let Some(alias) = alias { + write!(f, " as {}", alias)?; + } + + write!(f, ";") + } + UseItems::List(aliased_idents) => { + let _ = write!(f, "{{"); + for (i, (ident, alias)) in aliased_idents.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + + write!(f, "{}", ident)?; + + if let Some(alias) = alias { + write!(f, " as {}", alias)? + } + } + write!(f, "}};") + } + } + } +} + impl fmt::Display for FunctionParam { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.identifier(), self.ty()) @@ -874,7 +1008,11 @@ pub trait ParseFromStr: Sized { /// Trait for parsing with collection of errors. pub trait ParseFromStrWithErrors: Sized { /// Parse a value from the string `s` with Errors. - fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option; + fn parse_from_str_with_errors( + s: &str, + source: SourceFile, + handler: &mut ErrorCollector, + ) -> Option; } /// Trait for generating parsers of themselves. @@ -918,10 +1056,14 @@ impl ParseFromStr for A { } impl ParseFromStrWithErrors for A { - fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option { + fn parse_from_str_with_errors( + s: &str, + source: SourceFile, + handler: &mut ErrorCollector, + ) -> Option { let (tokens, lex_errs) = crate::lexer::lex(s); - handler.update(lex_errs); + handler.update_with_source_enrichment(source.clone(), lex_errs); let tokens = tokens?; let (ast, parse_errs) = A::parser() @@ -933,14 +1075,14 @@ impl ParseFromStrWithErrors for A { ) .into_output_errors(); - handler.update(parse_errs); + handler.update_with_source_enrichment(source.clone(), parse_errs); // TODO: We should return parsed result if we found errors, but because analyzing in `ast` module // is not handling poisoned tree right now, we don't return parsed result - if handler.get().is_empty() { - ast - } else { + if handler.has_errors() { None + } else { + ast } } } @@ -1138,7 +1280,12 @@ impl ChumskyParse for Program { let skip_until_next_item = any() .then( any() - .filter(|t| !matches!(t, Token::Fn | Token::Type | Token::Mod)) + .filter(|t| { + !matches!( + t, + Token::Pub | Token::Use | Token::Fn | Token::Type | Token::Mod + ) + }) .repeated(), ) // map to empty module @@ -1162,9 +1309,10 @@ impl ChumskyParse for Item { { let func_parser = Function::parser().map(Item::Function); let type_parser = TypeAlias::parser().map(Item::TypeAlias); + let use_parser = UseDecl::parser().map(Item::Use); let mod_parser = Module::parser().map(|_| Item::Module); - choice((func_parser, type_parser, mod_parser)) + choice((func_parser, use_parser, type_parser, mod_parser)) } } @@ -1173,6 +1321,12 @@ impl ChumskyParse for Function { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(|v| v.unwrap_or(Visibility::Private)) + .labelled("function visibility"); + let params = delimited_with_recovery( FunctionParam::parser() .separated_by(just(Token::Comma)) @@ -1204,12 +1358,14 @@ impl ChumskyParse for Function { ))) .labelled("function body"); - just(Token::Fn) - .ignore_then(FunctionName::parser()) + visibility + .then_ignore(just(Token::Fn)) + .then(FunctionName::parser()) .then(params) .then(ret) .then(body) - .map_with(|(((name, params), ret), body), e| Self { + .map_with(|((((visibility, name), params), ret), body), e| Self { + visibility, name, params, ret, @@ -1219,6 +1375,51 @@ impl ChumskyParse for Function { } } +impl ChumskyParse for UseDecl { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(|v| v.unwrap_or(Visibility::Private)); + + let path = Identifier::parser() + .then_ignore(just(Token::DoubleColon)) + .repeated() + .at_least(1) + .collect::>(); + + let aliased_item = Identifier::parser().then( + just(Token::As).ignore_then(Identifier::parser()).or_not(), // Returns None if 'as' missing' + ); + + let list = aliased_item + .clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .map(UseItems::List); + + let single = aliased_item.map(UseItems::Single); + let items = choice((list, single)); + + visibility + .then_ignore(just(Token::Use)) + .then(path) + .then(items) + .then_ignore(just(Token::Semi)) + .map_with(|((visibility, path), items), e| Self { + visibility, + path, + items, + span: e.span(), + }) + } +} + impl ChumskyParse for FunctionParam { fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where @@ -1347,16 +1548,14 @@ impl ChumskyParse for CallName { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { - let double_colon = just(Token::Colon).then(just(Token::Colon)).labelled("::"); - - let turbofish_start = double_colon.clone().then(just(Token::LAngle)).ignored(); + let turbofish_start = just(Token::DoubleColon).then(just(Token::LAngle)).ignored(); let generics_close = just(Token::RAngle); let type_cast = just(Token::LAngle) .ignore_then(AliasedType::parser()) .then_ignore(generics_close.clone()) - .then_ignore(just(Token::Colon).then(just(Token::Colon))) + .then_ignore(just(Token::DoubleColon)) .then_ignore(just(Token::Ident("into"))) .map(CallName::TypeCast); @@ -1461,14 +1660,23 @@ impl ChumskyParse for TypeAlias { where I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(|v| v.unwrap_or(Visibility::Private)); + let name = AliasName::parser().map_with(|name, e| (name, e.span())); - just(Token::Type) - .ignore_then(name) - .then_ignore(parse_token_with_recovery(Token::Eq)) - .then(AliasedType::parser()) - .then_ignore(just(Token::Semi)) - .map_with(|(name, ty), e| Self { + visibility + .then( + just(Token::Type) + .ignore_then(name) + .then_ignore(parse_token_with_recovery(Token::Eq)) + .then(AliasedType::parser()) + .then_ignore(just(Token::Semi)), + ) + .map_with(|(visibility, (name, ty)), e| Self { + visibility, name: name.0, ty, span: e.span(), @@ -1953,6 +2161,7 @@ impl crate::ArbitraryRec for Function { fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { use arbitrary::Arbitrary; + let visibility = Visibility::arbitrary(u)?; let name = FunctionName::arbitrary(u)?; let len = u.int_in_range(0..=3)?; let params = (0..len) @@ -1961,6 +2170,7 @@ impl crate::ArbitraryRec for Function { let ret = Option::::arbitrary(u)?; let body = Expression::arbitrary_rec(u, budget).map(Expression::into_block)?; Ok(Self { + visibility, name, params, ret, diff --git a/src/str.rs b/src/str.rs index a113a703..ddc280ec 100644 --- a/src/str.rs +++ b/src/str.rs @@ -115,6 +115,31 @@ impl<'a> arbitrary::Arbitrary<'a> for FunctionName { #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct Identifier(Arc); +impl From for Identifier { + fn from(alias: AliasName) -> Self { + // We move the inner Arc, so this is cheap + Self(alias.0) + } +} + +impl From for Identifier { + fn from(func: FunctionName) -> Self { + Self(func.0) + } +} + +impl From<&str> for Identifier { + fn from(s: &str) -> Self { + Self(Arc::from(s)) + } +} + +impl AsRef for Identifier { + fn as_ref(&self) -> &str { + &self.0 + } +} + wrapped_string!(Identifier, "variable identifier"); impl_arbitrary_lowercase_alpha!(Identifier); diff --git a/src/tracker.rs b/src/tracker.rs index 4a6f693a..562f95a6 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -400,6 +400,7 @@ mod tests { use crate::elements::pset::Input; use crate::elements::{AssetId, OutPoint, Script, Txid}; use crate::{Arguments, TemplateProgram, WitnessValues}; + use crate::{DependencyMap, SourceName}; use super::*; @@ -472,7 +473,12 @@ mod tests { #[test] fn test_debug_and_jet_tracing() { - let program = TemplateProgram::new(TEST_PROGRAM).unwrap(); + let program = TemplateProgram::new( + SourceName::default(), + Arc::from(DependencyMap::new()), + TEST_PROGRAM, + ) + .unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); @@ -541,7 +547,12 @@ mod tests { fn test_arith_jet_trace_regression() { let env = create_test_env(); - let program = TemplateProgram::new(TEST_ARITHMETIC_JETS).unwrap(); + let program = TemplateProgram::new( + SourceName::default(), + Arc::from(DependencyMap::new()), + TEST_ARITHMETIC_JETS, + ) + .unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); diff --git a/src/witness.rs b/src/witness.rs index 6d6ffefc..4ad7beb6 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt; use std::sync::Arc; -use crate::error::{Error, RichError, WithFile, WithSpan}; +use crate::error::{Error, RichError, WithSpan}; use crate::parse; use crate::parse::ParseFromStr; use crate::str::WitnessName; @@ -144,7 +144,7 @@ impl ParseFromStr for ResolvedType { .resolve_builtin() .map_err(Error::UndefinedAlias) .with_span(s) - .with_file(s) + // .with_source(s) // TODO: @LesterEvSe think about this deletion } } @@ -220,17 +220,24 @@ impl crate::ArbitraryOfType for Arguments { #[cfg(test)] mod tests { use super::*; + use crate::error::ErrorCollector; use crate::parse::ParseFromStr; use crate::value::ValueConstructible; - use crate::{ast, parse, CompiledProgram, SatisfiedProgram}; + use crate::{ast, driver, parse, CompiledProgram, DependencyMap, SatisfiedProgram}; + use crate::{SourceFile, SourceName}; #[test] fn witness_reuse() { let s = r#"fn main() { assert!(jet::eq_32(witness::A, witness::A)); }"#; + let source = SourceFile::new(SourceName::default(), Arc::from(s)); + let mut error_handler = ErrorCollector::new(); + let program = parse::Program::parse_from_str(s).expect("parsing works"); - match ast::Program::analyze(&program).map_err(Error::from) { + let driver_program = driver::Program::from_parse(&program, source, &mut error_handler) + .expect("driver works"); + match ast::Program::analyze(&driver_program).map_err(Error::from) { Ok(_) => panic!("Witness reuse was falsely accepted"), Err(Error::WitnessReused(..)) => {} Err(error) => panic!("Unexpected error: {error}"), @@ -247,7 +254,14 @@ mod tests { WitnessName::from_str_unchecked("A"), Value::u16(42), )])); - match SatisfiedProgram::new(s, Arguments::default(), witness, false) { + match SatisfiedProgram::new( + SourceName::default(), + Arc::from(DependencyMap::new()), + s, + Arguments::default(), + witness, + false, + ) { Ok(_) => panic!("Ill-typed witness assignment was falsely accepted"), Err(error) => assert_eq!( "Witness `A` was declared with type `u32` but its assigned value is of type `u16`", @@ -266,7 +280,13 @@ fn main() { assert!(jet::is_zero_32(f())); }"#; - match CompiledProgram::new(s, Arguments::default(), false) { + match CompiledProgram::new( + SourceName::default(), + Arc::from(DependencyMap::new()), + s, + Arguments::default(), + false, + ) { Ok(_) => panic!("Witness outside main was falsely accepted"), Err(error) => { assert!(error