diff --git a/Cargo.lock b/Cargo.lock index 6f6a4ea..352c0e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,15 +4,15 @@ version = 4 [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.2.16", + "getrandom", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -28,10 +28,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f5e3dca4e09a6f340a61a0e9c7b61e030c69fc27bf29d73218f7e5e3b7638f" dependencies = [ "concolor", - "unicode-width", + "unicode-width 0.1.14", "yansi", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "autocfg" version = "1.5.0" @@ -44,7 +50,9 @@ version = "0.1.0" dependencies = [ "ariadne", "internment", + "itertools", "memmap2", + "pretty", "proptest", "proptest-derive", "thiserror", @@ -73,15 +81,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "concolor" @@ -116,6 +124,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -152,25 +166,14 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" -version = "0.2.16" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", ] [[package]] @@ -210,26 +213,29 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "linux-raw-sys" @@ -239,19 +245,18 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -273,15 +278,15 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -290,28 +295,39 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.27", + "zerocopy", +] + +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec", + "termcolor", + "typed-arena", + "unicode-width 0.2.2", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.4", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand", "rand_chacha", @@ -341,9 +357,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -380,7 +396,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom", ] [[package]] @@ -394,18 +410,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rustix" @@ -413,7 +429,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -446,9 +462,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -462,12 +478,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom", "once_cell", "rustix", "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "2.0.17" @@ -488,6 +513,12 @@ dependencies = [ "syn", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "unarray" version = "0.1.4" @@ -496,9 +527,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" @@ -506,6 +537,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "version_check" version = "0.9.5" @@ -521,21 +558,6 @@ dependencies = [ "libc", ] -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -545,6 +567,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -557,16 +588,7 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -584,29 +606,13 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -615,90 +621,42 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -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.46.0" @@ -711,33 +669,13 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ - "zerocopy-derive 0.8.27", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "zerocopy-derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 364d469..35c2c6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,12 @@ edition = "2024" [dependencies] ariadne = { version = "0.5.1", features = ["auto-color"] } internment = { version = "0.8.6", features = ["arc", "arena"] } +itertools = "0.14.0" memmap2 = "0.9.8" +pretty = { version = "0.12.5", features = ["termcolor"] } proptest = "1.7.0" proptest-derive = "0.6.0" thiserror = "2.0.12" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] } diff --git a/src/syntax.rs b/src/syntax.rs index 08451b8..9edde17 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -1,3 +1,4 @@ +mod arbitrary; mod ast; mod error; mod location; @@ -5,6 +6,7 @@ mod name; mod parse; #[cfg(test)] mod parser_tests; +mod print; mod tokens; mod universe; diff --git a/src/syntax/arbitrary.rs b/src/syntax/arbitrary.rs new file mode 100644 index 0000000..107346d --- /dev/null +++ b/src/syntax/arbitrary.rs @@ -0,0 +1,548 @@ +use std::fmt::Arguments; + +use crate::syntax::ast::{ConstantValue, IntegerWithBase, Type}; +use crate::syntax::location::Location; +use crate::syntax::name::Name; +use itertools::Itertools; +use proptest::arbitrary::Arbitrary; +use proptest::prelude::{BoxedStrategy, Rng}; +use proptest::prop_oneof; +use proptest::strategy::{NewTree, Strategy, ValueTree}; +use proptest::test_runner::TestRunner; + +const MAXIMUM_TYPE_DEPTH: usize = 5; +const MAXIMUM_TYPE_WIDTH: usize = 5; +const MAXIMUM_STRING_SIZE: usize = 32; +const PRIMITIVE_TYPES: &[&str] = &[ + "Char", "String", "I8", "I16", "I32", "I64", "U8", "U16", "U32", "U64", +]; + +#[derive(Debug, Default)] +pub struct TypeGenerationContext { + available_constructors: Vec, + available_variables: Vec, +} + +impl TypeGenerationContext { + fn generate_type(&mut self, runner: &mut TestRunner, depth: usize) -> Type { + let mut leaf_options = vec![]; + + if !self.available_constructors.is_empty() { + for name in self.available_constructors.iter() { + leaf_options.push(Type::Constructor( + Location::manufactured(), + name.clone(), + )); + } + } + + if !self.available_variables.is_empty() { + for name in self.available_variables.iter() { + leaf_options.push(Type::Variable( + Location::manufactured(), + name.clone(), + )); + } + } + + for prim in PRIMITIVE_TYPES.iter() { + leaf_options.push(Type::Primitive( + Location::manufactured(), + Name::new(Location::manufactured(), prim.to_string()), + )); + } + + if depth < MAXIMUM_TYPE_DEPTH && runner.rng().random_bool(0.5) { + } + + let index = runner.rng().random_range(0..leaf_options.len()); + leaf_options.remove(index) + } +} + +#[derive(Clone)] +pub struct TypeGenerationTree { + current_value: Type, + parent: Option>, + untried_simplified_items: Option>, +} + +impl TypeGenerationTree { + /// Create a new type generation tree based on the given + /// initial value. + pub fn new(initial_value: Type) -> TypeGenerationTree { + TypeGenerationTree { + current_value: initial_value, + parent: None, + untried_simplified_items: None, + } + } +} + +fn generate_powerset(_: &[Type]) -> Vec> { + vec![] +} + +fn simplify_type(incoming: &Type) -> Vec { + match incoming { + Type::Primitive(_, _) => vec![], + Type::Constructor(_, _) => vec![], + Type::Variable(_, _) => vec![], + Type::Function(arg_types, ret_type) => { + let simplified_return_types = simplify_type(ret_type.as_ref()); + + // we do the following as a set of steps, choosing to go deep rather than + // broad immediately. So this works as follows: + // + // 1. If there are simplifications for the return type, then just + // return variations with the simplified return type. + // 2. If there are simplifications for the first argument, then + // just return variations with the first argument simplified. + // 3. Repeat for each of the arguments. + // 4. At this point, all the subtypes are as simple as they can + // be, so return a series of function types with fewer arguments. + // 5. If we are a function with no arguments, then just return + // the return type. + if !simplified_return_types.is_empty() { + return simplified_return_types + .into_iter() + .map(|ret| Type::Function(arg_types.clone(), Box::new(ret))) + .collect(); + } + + // now check the arguments, and see if we can simplify them in a + // better way. + for idx in 0..arg_types.len() { + let simplified_arguments = simplify_type(&arg_types[idx]); + + if simplified_arguments.is_empty() { + continue; + } + + let mut new_function_types = vec![]; + + for simplified_arg in simplified_arguments.into_iter() { + let mut new_args = vec![]; + + for item in &arg_types[0..idx] { + new_args.push(item.clone()); + } + new_args.push(simplified_arg); + for item in &arg_types[idx + 1..arg_types.len()] { + new_args.push(item.clone()); + } + + new_function_types.push(Type::Function(new_args, ret_type.clone())); + } + + if !new_function_types.is_empty() { + return new_function_types; + } + } + + // ok, all of the arguments and the return type are already as + // simple as they can be, so let's see if we can reduce the number + // of arguments. + let mut new_types = vec![]; + for args in arg_types.iter().powerset() { + if args.len() != arg_types.len() { + new_types.push(Type::Function( + args.into_iter().cloned().collect(), + ret_type.clone(), + )); + } + } + + if new_types.is_empty() { + vec![ret_type.as_ref().clone()] + } else { + new_types + } + } + + Type::Application(constructor_type, arg_types) => { + // much like functions, we're going to try to simplify the constructor, + // then we'll try to simplify the arguments, then we'll try to remove + // arguments. + let simplified_constructor = simplify_type(constructor_type.as_ref()); + + if !simplified_constructor.is_empty() { + return simplified_constructor + .into_iter() + .map(|c| Type::Application(Box::new(c), arg_types.clone())) + .collect(); + } + + // now check the arguments, and see if we can simplify them in a + // better way. + for idx in 0..arg_types.len() { + let simplified_arguments = simplify_type(&arg_types[idx]); + + if simplified_arguments.is_empty() { + continue; + } + + let mut new_appl_types = vec![]; + + for simplified_arg in simplified_arguments.into_iter() { + let mut new_args = vec![]; + + for item in &arg_types[0..idx] { + new_args.push(item.clone()); + } + new_args.push(simplified_arg); + for item in &arg_types[idx + 1..arg_types.len()] { + new_args.push(item.clone()); + } + + new_appl_types.push(Type::Application(constructor_type.clone(), new_args)); + } + + if !new_appl_types.is_empty() { + return new_appl_types; + } + } + + // and now we'll try to reduce types. + let mut new_types = vec![]; + for args in arg_types.iter().powerset() { + if args.len() != arg_types.len() { + new_types.push(Type::Application( + constructor_type.clone(), + args.into_iter().cloned().collect(), + )); + } + } + + if new_types.is_empty() { + vec![constructor_type.as_ref().clone()] + } else { + new_types + } + } + } +} + +impl ValueTree for TypeGenerationTree { + type Value = Type; + + fn current(&self) -> Self::Value { + self.current_value.clone() + } + + fn simplify(&mut self) -> bool { + match self.untried_simplified_items.as_mut() { + None => { + let mut simplified = simplify_type(&self.current_value) + .into_iter() + .map(|current_value| TypeGenerationTree { + current_value, + parent: Some(Box::new(self.clone())), + + untried_simplified_items: None, + }) + .collect::>(); + + match simplified.pop() { + None => { + self.untried_simplified_items = Some(simplified); + false + } + + Some(next_tree) => { + self.untried_simplified_items = Some(simplified); + *self = next_tree; + true + } + } + } + + Some(untried_simplifieds) => match untried_simplifieds.pop() { + None => false, + Some(x) => { + *self = x; + true + } + }, + } + } + + fn complicate(&mut self) -> bool { + match self.parent.take() { + None => false, + Some(x) => { + *self = *x; + true + } + } + } +} + +impl Strategy for TypeGenerationContext { + type Tree = TypeGenerationTree; + type Value = Type; + + fn new_tree(&self, _runner: &mut TestRunner) -> NewTree { + unimplemented!() + } +} + +impl Arbitrary for Type { + type Parameters = TypeGenerationContext; + type Strategy = TypeGenerationContext; + + fn arbitrary_with(_context: Self::Parameters) -> Self::Strategy { + unimplemented!() + } +} + +#[derive(Default)] +pub enum LegalConstantType { + #[default] + Any, + String, + Char, + Number, +} + +impl Arbitrary for ConstantValue { + type Parameters = LegalConstantType; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + match args { + LegalConstantType::Char => char::arbitrary() + .prop_map(|x| ConstantValue::Character(Location::manufactured(), x)) + .boxed(), + + LegalConstantType::String => { + proptest::collection::vec(proptest::char::any(), MAXIMUM_STRING_SIZE) + .prop_map(|x| { + ConstantValue::String(Location::manufactured(), String::from_iter(x)) + }) + .boxed() + } + + LegalConstantType::Number => { + let value_strat = u64::arbitrary(); + let base_strat = proptest::prop_oneof![ + proptest::strategy::Just(None), + proptest::strategy::Just(Some(2)), + proptest::strategy::Just(Some(8)), + proptest::strategy::Just(Some(10)), + proptest::strategy::Just(Some(16)), + ]; + + (value_strat, base_strat) + .prop_map(|(value, base)| { + ConstantValue::Integer( + Location::manufactured(), + IntegerWithBase { base, value }, + ) + }) + .boxed() + } + + LegalConstantType::Any => proptest::prop_oneof![ + Self::arbitrary_with(LegalConstantType::Char), + Self::arbitrary_with(LegalConstantType::String), + Self::arbitrary_with(LegalConstantType::Number), + ] + .boxed(), + } + } +} + +#[cfg(test)] +mod simplifiers { + use super::*; + + #[test] + fn types() { + let loc = Location::manufactured(); + let foo = Name::new(loc.clone(), "Foo"); + let primint = Type::Primitive(loc.clone(), Name::new(loc.clone(), "Int")); + let primchar = Type::Primitive(loc.clone(), Name::new(loc.clone(), "Char")); + let primstr = Type::Primitive(loc.clone(), Name::new(loc.clone(), "String")); + + assert_eq!( + simplify_type(&Type::Constructor(loc.clone(), foo.clone())), + vec![] + ); + assert_eq!( + simplify_type(&Type::Variable(loc.clone(), foo.clone())), + vec![] + ); + assert_eq!( + simplify_type(&Type::Primitive(loc.clone(), foo.clone())), + vec![] + ); + + assert_eq!( + simplify_type(&Type::Function(vec![], Box::new(primint.clone()))), + vec![primint.clone()] + ); + assert_eq!( + simplify_type(&Type::Function( + vec![primint.clone(), primchar.clone()], + Box::new(primint.clone()) + )), + vec![ + Type::Function(vec![], Box::new(primint.clone())), + Type::Function(vec![primint.clone()], Box::new(primint.clone())), + Type::Function(vec![primchar.clone()], Box::new(primint.clone())), + ] + ); + assert_eq!( + simplify_type(&Type::Function( + vec![primint.clone(), primchar.clone(), primstr.clone()], + Box::new(primint.clone()) + )), + vec![ + Type::Function(vec![], Box::new(primint.clone())), + Type::Function(vec![primint.clone()], Box::new(primint.clone())), + Type::Function(vec![primchar.clone()], Box::new(primint.clone())), + Type::Function(vec![primstr.clone()], Box::new(primint.clone())), + Type::Function( + vec![primint.clone(), primchar.clone()], + Box::new(primint.clone()) + ), + Type::Function( + vec![primint.clone(), primstr.clone()], + Box::new(primint.clone()) + ), + Type::Function( + vec![primchar.clone(), primstr.clone()], + Box::new(primint.clone()) + ), + ] + ); + + assert_eq!( + simplify_type(&Type::Function( + vec![primint.clone(), primchar.clone(), primstr.clone()], + Box::new(Type::Function(vec![], Box::new(primint.clone()))), + )), + vec![Type::Function( + vec![primint.clone(), primchar.clone(), primstr.clone()], + Box::new(primint.clone()) + ),] + ); + assert_eq!( + simplify_type(&Type::Function( + vec![primint.clone(), primchar.clone(), primstr.clone()], + Box::new(Type::Function( + vec![primint.clone(), primchar.clone()], + Box::new(primint.clone()) + )), + )), + vec![ + Type::Function( + vec![primint.clone(), primchar.clone(), primstr.clone()], + Box::new(Type::Function(vec![], Box::new(primint.clone()))) + ), + Type::Function( + vec![primint.clone(), primchar.clone(), primstr.clone()], + Box::new(Type::Function( + vec![primint.clone()], + Box::new(primint.clone()) + )) + ), + Type::Function( + vec![primint.clone(), primchar.clone(), primstr.clone()], + Box::new(Type::Function( + vec![primchar.clone()], + Box::new(primint.clone()) + )) + ), + ] + ); + assert_eq!( + simplify_type(&Type::Function( + vec![ + Type::Function(vec![], Box::new(primint.clone())), + primstr.clone() + ], + Box::new(primint.clone()) + )), + vec![Type::Function( + vec![primint.clone(), primstr.clone()], + Box::new(primint.clone()) + )] + ); + assert_eq!( + simplify_type(&Type::Function( + vec![ + primint.clone(), + Type::Function(vec![], Box::new(primint.clone())) + ], + Box::new(primint.clone()) + )), + vec![Type::Function( + vec![primint.clone(), primint.clone()], + Box::new(primint.clone()) + )] + ); + + let applied = Type::Application(Box::new(primint.clone()), vec![]); + assert_eq!( + simplify_type(&Type::Application(Box::new(primint.clone()), vec![])), + vec![primint.clone()] + ); + assert_eq!(simplify_type(&applied), vec![primint.clone()]); + assert_eq!( + simplify_type(&Type::Application( + Box::new(applied.clone()), + vec![primint.clone()] + )), + vec![Type::Application( + Box::new(primint.clone()), + vec![primint.clone()] + )] + ); + assert_eq!( + simplify_type(&Type::Application( + Box::new(primint.clone()), + vec![applied.clone()] + )), + vec![Type::Application( + Box::new(primint.clone()), + vec![primint.clone()] + )] + ); + assert_eq!( + simplify_type(&Type::Application( + Box::new(primint.clone()), + vec![primchar.clone(), applied.clone(), primstr.clone()] + )), + vec![Type::Application( + Box::new(primint.clone()), + vec![primchar.clone(), primint.clone(), primstr.clone()] + )] + ); + assert_eq!( + simplify_type(&Type::Application( + Box::new(primint.clone()), + vec![primchar.clone(), primint.clone(), primstr.clone()] + )), + vec![ + Type::Application(Box::new(primint.clone()), vec![]), + Type::Application(Box::new(primint.clone()), vec![primchar.clone()]), + Type::Application(Box::new(primint.clone()), vec![primint.clone()]), + Type::Application(Box::new(primint.clone()), vec![primstr.clone()]), + Type::Application( + Box::new(primint.clone()), + vec![primchar.clone(), primint.clone()] + ), + Type::Application( + Box::new(primint.clone()), + vec![primchar.clone(), primstr.clone()] + ), + Type::Application( + Box::new(primint.clone()), + vec![primint.clone(), primstr.clone()] + ) + ] + ); + } +} diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index fc78da6..64837b9 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -244,7 +244,7 @@ pub struct TypeRestriction { pub arguments: Vec, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum Type { Constructor(Location, Name), Variable(Location, Name), @@ -253,6 +253,22 @@ pub enum Type { Function(Vec, Box), } +impl PartialEq for Type { + fn eq(&self, other: &Self) -> bool { + match self { + Type::Constructor(_, x) => matches!(other, Type::Constructor(_, y) if x == y), + Type::Variable(_, x) => matches!(other, Type::Variable(_, y) if x == y), + Type::Primitive(_, x) => matches!(other, Type::Primitive(_, y) if x == y), + Type::Application(con1, args1) => { + matches!(other, Type::Application(con2, args2) if con1 == con2 && args1 == args2) + } + Type::Function(args1, ret1) => { + matches!(other, Type::Function(args2, ret2) if args1 == args2 && ret1 == ret2) + } + } + } +} + impl Located for Type { fn location(&self) -> Location { match self { @@ -277,7 +293,7 @@ impl Located for Type { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum ConstantValue { Integer(Location, IntegerWithBase), Character(Location, char), @@ -294,6 +310,18 @@ impl Located for ConstantValue { } } +impl PartialEq for ConstantValue { + fn eq(&self, other: &Self) -> bool { + match self { + ConstantValue::Character(_, x) => { + matches!(other, ConstantValue::Character(_, y) if x == y) + } + ConstantValue::String(_, x) => matches!(other, ConstantValue::String(_, y) if x == y), + ConstantValue::Integer(_, x) => matches!(other, ConstantValue::Integer(_, y) if x == y), + } + } +} + #[derive(Clone, Debug, PartialEq, Eq, Arbitrary)] pub struct IntegerWithBase { #[proptest(strategy = "proptest::prop_oneof![ \ diff --git a/src/syntax/location.rs b/src/syntax/location.rs index cd8bc78..2bc59f0 100644 --- a/src/syntax/location.rs +++ b/src/syntax/location.rs @@ -38,6 +38,13 @@ impl Location { } } + pub fn manufactured() -> Self { + Location { + file: ArcIntern::new("".into()), + span: 0..0, + } + } + pub fn extend_to(&self, other: &Location) -> Location { assert_eq!(self.file, other.file); Location { @@ -50,12 +57,23 @@ impl Location { self.span = min(self.span.start, span.start)..max(self.span.end, span.end); self } - - pub fn file(&self) -> &str { - self.file.to_str().unwrap_or("") - } - - pub fn span(&self) -> Range { - self.span.clone() - } +} + +#[test] +fn extension_and_merge() { + let file = ArcIntern::new("/foo/bar.txt".into()); + let loc1 = Location::new(&file, 1..4); + let loc2 = Location::new(&file, 4..8); + + assert_eq!(loc1.extend_to(&loc2).source(), &file); + assert_eq!(loc1.extend_to(&loc2).start(), 1); + assert_eq!(loc1.extend_to(&loc2).end(), 8); + + let loc3 = Location::new(&file, 12..16); + assert_eq!(loc1.extend_to(&loc3).source(), &file); + assert_eq!(loc1.extend_to(&loc3).start(), 1); + assert_eq!(loc1.extend_to(&loc3).end(), 16); + + assert_eq!(loc1.clone().merge_span(0..1).start(), 0); + assert_eq!(loc1.merge_span(0..1).end(), 4); } diff --git a/src/syntax/name.rs b/src/syntax/name.rs index 7b48d51..ad7ef8c 100644 --- a/src/syntax/name.rs +++ b/src/syntax/name.rs @@ -1,12 +1,14 @@ use crate::syntax::Location; +#[cfg(test)] +use internment::ArcIntern; use std::cmp; use std::fmt; -use std::hash; +use std::hash::{Hash, Hasher}; use std::sync::atomic::{AtomicU64, Ordering}; static IDENTIFIER_COUNTER: AtomicU64 = AtomicU64::new(0); -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Name { printable: String, identifier: u64, @@ -21,8 +23,8 @@ impl cmp::PartialEq for Name { impl cmp::Eq for Name {} -impl hash::Hash for Name { - fn hash(&self, state: &mut H) { +impl Hash for Name { + fn hash(&self, state: &mut H) { self.identifier.hash(state); } } @@ -66,3 +68,89 @@ impl Name { self.location.as_ref() } } + +#[test] +fn equality() { + let file = ArcIntern::new("/foo.bang".into()); + let loc1 = Location::new(&file, 0..3); + let loc2 = Location::new(&file, 9..12); + + assert_ne!(Name::gensym("x"), Name::gensym("x")); + assert_ne!(Name::new(loc1.clone(), "x"), Name::new(loc1.clone(), "x")); + assert_eq!( + Name { + printable: "x".into(), + identifier: 5, + location: Some(loc1.clone()) + }, + Name { + printable: "x".into(), + identifier: 5, + location: Some(loc2.clone()) + } + ); + assert_eq!( + Name { + printable: "x".into(), + identifier: 5, + location: Some(loc1.clone()) + }, + Name { + printable: "x".into(), + identifier: 5, + location: None + } + ); + assert_eq!( + Name { + printable: "x".into(), + identifier: 5, + location: Some(loc1.clone()) + }, + Name { + printable: "y".into(), + identifier: 5, + location: None + } + ); +} + +#[test] +fn hashing() { + let file = ArcIntern::new("/foo.bang".into()); + let loc1 = Location::new(&file, 0..3); + let loc2 = Location::new(&file, 9..12); + + let x1 = Name { + printable: "x".into(), + identifier: 1, + location: Some(loc1), + }; + let mut x2 = Name { + printable: "x".into(), + identifier: 2, + location: Some(loc2), + }; + let y1 = Name { + printable: "y".into(), + identifier: 1, + location: None, + }; + + let run_hash = |name: &Name| { + let mut hash = std::hash::DefaultHasher::new(); + name.hash(&mut hash); + hash.finish() + }; + + let hash_x1 = run_hash(&x1); + let hash_x2 = run_hash(&x2); + let hash_y1 = run_hash(&y1); + + assert_ne!(hash_x1, hash_x2); + assert_eq!(hash_x1, hash_y1); + + x2.bind_to(&x1); + let rehashed_x2 = run_hash(&x2); + assert_eq!(hash_x1, rehashed_x2); +} diff --git a/src/syntax/parse.rs b/src/syntax/parse.rs index ae49b4a..5025c5e 100644 --- a/src/syntax/parse.rs +++ b/src/syntax/parse.rs @@ -261,9 +261,13 @@ impl<'lexer> Parser<'lexer> { } #[allow(unused)] + #[cfg(not(coverage))] fn print_next_token(&mut self, comment: &str) { let token = self.next().expect("can get token"); - println!("[{comment}] next token will be {:?}", token.as_ref().map(|x| x.token.clone())); + println!( + "[{comment}] next token will be {:?}", + token.as_ref().map(|x| x.token.clone()) + ); if let Some(token) = token { self.save(token); } @@ -379,14 +383,7 @@ impl<'lexer> Parser<'lexer> { arguments, }; - let Some(maybe_comma) = self.next()? else { - return Ok(Some(restriction)); - }; - - match maybe_comma.token { - Token::Comma => {} - _ => self.save(maybe_comma), - } + let _ = self.require_token(Token::Comma, ""); Ok(Some(restriction)) } @@ -402,30 +399,37 @@ impl<'lexer> Parser<'lexer> { let next = self .next()? .ok_or_else(|| self.bad_eof("looking for definition body"))?; - self.save(next.clone()); - if let Ok(structure) = self.parse_structure() { - return Ok(Def::Structure(structure)); + match next.token { + Token::ValueName(ref x) if x == "structure" => { + self.save(next); + Ok(Def::Structure(self.parse_structure()?)) + } + + Token::ValueName(ref x) if x == "enumeration" => { + self.save(next); + Ok(Def::Enumeration(self.parse_enumeration()?)) + } + + Token::ValueName(ref x) + if x == "operator" || x == "prefix" || x == "infix" || x == "postfix" => + { + self.save(next); + Ok(Def::Operator(self.parse_operator()?)) + } + + Token::ValueName(_) => { + self.save(next); + self.parse_function_or_value() + } + + _ => Err(ParserError::UnexpectedToken { + file: self.file.clone(), + span: next.span, + token: next.token, + expected: "'structure', 'enumeration', 'operator', or a value identifier".into(), + }), } - - if let Ok(enumeration) = self.parse_enumeration() { - return Ok(Def::Enumeration(enumeration)); - } - - if let Ok(operator) = self.parse_operator() { - return Ok(Def::Operator(operator)); - } - - if let Ok(fun_or_val) = self.parse_function_or_value() { - return Ok(fun_or_val); - } - - Err(ParserError::UnexpectedToken { - file: self.file.clone(), - span: next.span, - token: next.token, - expected: "'structure', 'enumeration', or a value identifier".into(), - }) } /// Parse a structure definition. @@ -821,6 +825,7 @@ impl<'lexer> Parser<'lexer> { fn parse_function_def_arguments(&mut self) -> Result, ParserError> { let _ = self.require_token(Token::OpenParen, "start of function argument definition")?; let mut result = vec![]; + let mut just_skipped_comma = false; loop { let next = self @@ -831,7 +836,22 @@ impl<'lexer> Parser<'lexer> { break; } + if matches!(next.token, Token::Comma) { + if just_skipped_comma { + return Err(ParserError::UnexpectedToken { + file: self.file.clone(), + span: next.span, + token: next.token, + expected: "after another comma in function arguments".into(), + }); + } + + just_skipped_comma = true; + continue; + } + self.save(next); + just_skipped_comma = false; let name = self.parse_name("function argument name")?; let mut arg_type = None; @@ -992,6 +1012,7 @@ impl<'lexer> Parser<'lexer> { Some(Box::new(sub_pattern)) } else { + self.save(maybe_paren); None } } else { @@ -1410,9 +1431,12 @@ impl<'lexer> Parser<'lexer> { Token::TypeName(n) | Token::PrimitiveTypeName(n) => { let type_name = Name::new(self.to_location(next.span.clone()), n); - let after_type_name = self.next()?.ok_or_else(|| { - self.bad_eof("looking for colon, open brace, or open paren in constructor") - })?; + let Some(after_type_name) = self.next()? else { + return Ok(Expression::Reference( + type_name.location().unwrap().clone(), + type_name, + )); + }; match after_type_name.token { Token::OpenBrace => { @@ -1480,12 +1504,13 @@ impl<'lexer> Parser<'lexer> { Ok(Expression::Enumeration(ev)) } - _ => Err(ParserError::UnexpectedToken { - file: self.file.clone(), - span: after_type_name.span, - token: after_type_name.token, - expected: "colon, open brace, or open paren in constructor".into(), - }), + _ => { + self.save(after_type_name); + Ok(Expression::Reference( + type_name.location().unwrap().clone(), + type_name, + )) + } } } diff --git a/src/syntax/parser_tests.rs b/src/syntax/parser_tests.rs index 2219d7e..0a214a4 100644 --- a/src/syntax/parser_tests.rs +++ b/src/syntax/parser_tests.rs @@ -114,6 +114,9 @@ fn types() { if b1.as_printed() == "a" && b2.as_printed() == "b")) && matches!(z.as_ref(), Type::Variable(_, z1) if z1.as_printed() == "z") )); + assert!(parse_type("Cons a b ->").is_err()); + assert!(parse_type("(Cons a b) (Cons a b)").is_err()); + assert!(parse_type("(Cons a b) (Cons a b) :").is_err()); } #[test] @@ -129,6 +132,17 @@ fn type_restrictions() { Ok(TypeRestrictions{ restrictions }) if restrictions.is_empty() )); + assert!(matches!( + parse_tr("restrict(prim%Cons a b)"), + Ok(TypeRestrictions { restrictions }) if restrictions.len() == 1 && + matches!(&restrictions[0], TypeRestriction { + constructor, + arguments, + } if matches!(constructor, Type::Primitive(_, x) if x.as_printed() == "Cons") && + arguments.len() == 2 && + matches!(&arguments[0], Type::Variable(_, x) if x.as_printed() == "a") && + matches!(&arguments[1], Type::Variable(_, x) if x.as_printed() == "b")))); + assert!(matches!( parse_tr("restrict(Cons a b)"), Ok(TypeRestrictions { restrictions }) if restrictions.len() == 1 && @@ -151,6 +165,8 @@ fn type_restrictions() { matches!(&arguments[0], Type::Variable(_, x) if x.as_printed() == "a") && matches!(&arguments[1], Type::Variable(_, x) if x.as_printed() == "b")))); + assert!(parse_tr("restrict(cons a b,)").is_err()); + assert!(parse_tr("restrict(,Cons a b,)").is_err()); assert!(matches!( @@ -216,6 +232,9 @@ fn field_definition() { if matches!(c.as_ref(), Type::Constructor(_, c) if c.as_printed() == "Word8") && args.is_empty()))); + assert!(parse_fd("foo :: Word8,").is_err()); + assert!(parse_fd("foo: Word8;").is_err()); + assert!(matches!( parse_fd("foo: Cons a b,"), Ok(Some(StructureField{ name, field_type, .. })) @@ -400,6 +419,8 @@ fn enumerations() { EnumerationVariant { name: name2, argument: arg2, ..}, ] if name1.as_printed() == "A" && arg1.is_none() && name2.as_printed() == "B" && arg2.is_none()))); + assert!(parse_en("enumeration Alternates { A").is_err()); + assert!(parse_en("enumeration Alternates { A; B }").is_err()); assert!(matches!( parse_en("enumeration Alternates { A, B, }"), Ok(EnumerationDef { name, variants, .. }) @@ -423,6 +444,9 @@ fn expressions() { assert!(matches!( parse_ex("x"), Ok(Expression::Reference(_,n)) if n.as_printed() == "x")); + assert!(matches!( + parse_ex("X"), + Ok(Expression::Reference(_,n)) if n.as_printed() == "X")); assert!(matches!( parse_ex("(x)"), Ok(Expression::Reference(_,n)) if n.as_printed() == "x")); @@ -453,6 +477,7 @@ fn enumeration_values() { }; assert!(parse_ex("Hello::world").is_err()); + assert!(parse_ex("Hello::world(a,b)").is_err()); assert!(matches!( parse_ex("Hello::World"), Ok(Expression::Enumeration(ev)) @@ -465,6 +490,14 @@ fn enumeration_values() { if ev.type_name.as_printed() == "Hello" && ev.variant_name.as_printed() == "World" && ev.argument.is_some())); + assert!(matches!( + parse_ex("Hello::World + 1"), + Ok(Expression::Call(plus, CallKind::Infix, args)) if + matches!(plus.as_ref(), Expression::Reference(_, n) if n.as_printed() == "+") && + matches!(args.as_slice(), [ + Expression::Enumeration(_), + Expression::Value(_) + ]))); } #[test] @@ -475,6 +508,7 @@ fn structure_value() { result.parse_expression() }; + assert!(parse_st("Foo{").is_err()); assert!(parse_st("Foo{ , }").is_err()); assert!(parse_st("Foo{ foo, }").is_err()); assert!(parse_st("Foo{ foo: , }").is_err()); @@ -635,6 +669,11 @@ fn calls() { Ok(Expression::Call(f, CallKind::Normal, args)) if matches!(f.as_ref(), Expression::Reference(_,n) if n.as_printed() == "f") && args.is_empty())); + assert!(parse_ex("f(").is_err()); + assert!(parse_ex("f(a").is_err()); + assert!(parse_ex("f(a,b").is_err()); + assert!(parse_ex("f(a,b,").is_err()); + assert!(parse_ex("f(a,b ::").is_err()); assert!(matches!( parse_ex("f(a)"), Ok(Expression::Call(f, CallKind::Normal, args)) if @@ -656,6 +695,14 @@ fn calls() { Expression::Reference(_,a), Expression::Reference(_,b), ] if a.as_printed() == "a" && b.as_printed() == "b"))); + assert!(matches!( + parse_ex("f(A,b,)"), + Ok(Expression::Call(f, CallKind::Normal, args)) if + matches!(f.as_ref(), Expression::Reference(_,n) if n.as_printed() == "f") && + matches!(args.as_slice(), [ + Expression::Reference(_,a), + Expression::Reference(_,b), + ] if a.as_printed() == "A" && b.as_printed() == "b"))); assert!(parse_ex("f(,a,b,)").is_err()); assert!(parse_ex("f(a,,b,)").is_err()); assert!(parse_ex("f(a,b,,)").is_err()); @@ -833,6 +880,9 @@ fn prefix_and_postfix() { parse_ex("a * ++ b"), Err(ParserError::UnexpectedToken{ token: Token::OperatorName(pp), .. }) if pp == "++")); + + // this is a little bit of a weird case. + assert!(parse_ex("**").is_err()); } #[test] @@ -1083,6 +1133,154 @@ fn definitions() { body.len() == 1))); } +#[test] +fn functions() { + let parse_def = |str| { + let lexer = Lexer::from(str); + let mut result = Parser::new("test", lexer); + result.parse_definition() + }; + + assert!(matches!( + parse_def("function() { }"), + Ok(Definition { export: ExportClass::Private, type_restrictions, definition, .. }) if + type_restrictions.is_empty() && + matches!(&definition, Def::Function(FunctionDef { name, arguments, return_type, body, .. }) if + name.as_printed() == "function" && + arguments.is_empty() && + return_type.is_none() && + body.len() == 1))); + assert!(matches!( + parse_def("fun(a) { }"), + Ok(Definition { export: ExportClass::Private, type_restrictions, definition, .. }) if + type_restrictions.is_empty() && + matches!(&definition, Def::Function(FunctionDef { name, arguments, return_type, body, .. }) if + name.as_printed() == "fun" && + return_type.is_none() && + body.len() == 1 && + matches!(arguments.as_slice(), [FunctionArg{ name, arg_type: None }] if + name.as_printed() == "a")))); + assert!(matches!( + parse_def("fun(a,b) { }"), + Ok(Definition { export: ExportClass::Private, type_restrictions, definition, .. }) if + type_restrictions.is_empty() && + matches!(&definition, Def::Function(FunctionDef { name, arguments, return_type, body, .. }) if + name.as_printed() == "fun" && + return_type.is_none() && + body.len() == 1 && + matches!(arguments.as_slice(), [ + FunctionArg{ name: aname, arg_type: None }, + FunctionArg{ name: bname, arg_type: None } + ] if + aname.as_printed() == "a" && + bname.as_printed() == "b")))); + assert!(matches!( + parse_def("fun(a,b,) { }"), + Ok(Definition { export: ExportClass::Private, type_restrictions, definition, .. }) if + type_restrictions.is_empty() && + matches!(&definition, Def::Function(FunctionDef { name, arguments, return_type, body, .. }) if + name.as_printed() == "fun" && + return_type.is_none() && + body.len() == 1 && + matches!(arguments.as_slice(), [ + FunctionArg{ name: aname, arg_type: None }, + FunctionArg{ name: bname, arg_type: None } + ] if + aname.as_printed() == "a" && + bname.as_printed() == "b")))); + assert!(matches!( + parse_def("fun(a:U8,b,) { }"), + Ok(Definition { export: ExportClass::Private, type_restrictions, definition, .. }) if + type_restrictions.is_empty() && + matches!(&definition, Def::Function(FunctionDef { name, arguments, return_type, body, .. }) if + name.as_printed() == "fun" && + return_type.is_none() && + body.len() == 1 && + matches!(arguments.as_slice(), [ + FunctionArg{ name: aname, arg_type: Some(Type::Application(atype, atype_args)) }, + FunctionArg{ name: bname, arg_type: None } + ] if + aname.as_printed() == "a" && + matches!(atype.as_ref(), Type::Constructor(_, n) if n.as_printed() == "U8") && + atype_args.is_empty() && + bname.as_printed() == "b")))); + assert!(matches!( + parse_def("fun(a:U8,b:U8,) { }"), + Ok(Definition { export: ExportClass::Private, type_restrictions, definition, .. }) if + type_restrictions.is_empty() && + matches!(&definition, Def::Function(FunctionDef { name, arguments, return_type, body, .. }) if + name.as_printed() == "fun" && + return_type.is_none() && + body.len() == 1 && + matches!(arguments.as_slice(), [ + FunctionArg{ name: aname, arg_type: Some(Type::Application(atype, atype_args)) }, + FunctionArg{ name: bname, arg_type: Some(Type::Application(btype, btype_args)) } + ] if + aname.as_printed() == "a" && + matches!(atype.as_ref(), Type::Constructor(_, n) if n.as_printed() == "U8") && + atype_args.is_empty() && + bname.as_printed() == "b" && + matches!(btype.as_ref(), Type::Constructor(_, n) if n.as_printed() == "U8") && + btype_args.is_empty())))); + assert!(matches!( + parse_def("fun(a,b:U8,) { }"), + Ok(Definition { export: ExportClass::Private, type_restrictions, definition, .. }) if + type_restrictions.is_empty() && + matches!(&definition, Def::Function(FunctionDef { name, arguments, return_type, body, .. }) if + name.as_printed() == "fun" && + return_type.is_none() && + body.len() == 1 && + matches!(arguments.as_slice(), [ + FunctionArg{ name: aname, arg_type: None }, + FunctionArg{ name: bname, arg_type: Some(Type::Application(btype, btype_args)) } + ] if + aname.as_printed() == "a" && + bname.as_printed() == "b" && + matches!(btype.as_ref(), Type::Constructor(_, n) if n.as_printed() == "U8") && + btype_args.is_empty())))); + assert!(parse_def("fun(a,,b,) { }").is_err()); +} + +#[test] +fn definition_types() { + let parse_def = |str| { + let lexer = Lexer::from(str); + let mut result = Parser::new("test", lexer); + result.parse_definition() + }; + + assert!(matches!( + parse_def("x: prim%U8 = 1;"), + Ok(Definition { definition, .. }) if + matches!(&definition, Def::Value(ValueDef{ mtype, .. }) if + matches!(mtype, Some(Type::Application(f, args)) if + args.is_empty() && + matches!(f.as_ref(), Type::Primitive(_, name) if + name.as_printed() == "U8"))))); + assert!(matches!( + parse_def("x: Stupid Monad prim%U8 = 1;"), + Ok(Definition { definition, .. }) if + matches!(&definition, Def::Value(ValueDef{ mtype, .. }) if + matches!(mtype, Some(Type::Application(f, args)) if + matches!(f.as_ref(), Type::Constructor(_, name) if + name.as_printed() == "Stupid") && + matches!(args.as_slice(), [Type::Constructor(_, cname), Type::Primitive(_, pname)] if + cname.as_printed() == "Monad" && + pname.as_printed() == "U8"))))); + assert!(matches!( + parse_def("x: Stupid (Monad prim%U8) = 1;"), + Ok(Definition { definition, .. }) if + matches!(&definition, Def::Value(ValueDef{ mtype, .. }) if + matches!(mtype, Some(Type::Application(f, args)) if + matches!(f.as_ref(), Type::Constructor(_, name) if + name.as_printed() == "Stupid") && + matches!(args.as_slice(), [Type::Application(cname, args2)] if + matches!(cname.as_ref(), Type::Constructor(_, c) if c.as_printed() == "Monad") && + matches!(args2.as_slice(), [Type::Primitive(_, pname)] if + pname.as_printed() == "U8")))))); + assert!(parse_def("x: Stupid (Monad prim%U8 = 1;").is_err()); +} + #[test] fn operators() { let parse = |str| { @@ -1108,16 +1306,21 @@ prefix operator $$ at 1 -> money; assert!(parse("right infix operator - -> negate;").is_err()); assert!(parse("left infix operator - -> negate;").is_err()); assert!(parse("infix operator at 8 - -> negate;").is_err()); - + assert!(parse("infix operator * at 16 -> multiply;").is_err()); + assert!(parse("infix operator * at apple -> multiply;").is_err()); // these are designed to replicate the examples in the infix_and_precedence // tests, but with the precedence set automatically by the parser. - let plus_and_times = |expr| format!(r#" + let plus_and_times = |expr| { + format!( + r#" infix left operator + at 6 -> add; infix right operator * at 7 -> mul; x = {expr}; -"#); +"# + ) + }; let plus_example = plus_and_times("1 + 2 + 3"); assert!(matches!( @@ -1155,7 +1358,6 @@ x = {expr}; Expression::Value(ConstantValue::Integer(_, IntegerWithBase{ value: v3, .. })) ] if *v2 == 2 && *v3 == 3))))))); - let mixed_example = plus_and_times("1 + 2 * 3 + 4"); assert!(matches!( parse(&mixed_example), @@ -1183,3 +1385,116 @@ x = {expr}; matches!(v2, IntegerWithBase{ value: 2, .. }) && matches!(v3, IntegerWithBase{ value: 3, .. }))))))))); } + +#[test] +fn pattern_match() { + let parse_ex = |str| { + let lexer = Lexer::from(str); + let mut result = Parser::new("test", lexer); + result.parse_expression() + }; + + assert!(matches!( + parse_ex("match x { }"), + Ok(Expression::Match(MatchExpr{ value, cases, .. })) if + matches!(value.as_ref(), Expression::Reference(_, n) if n.as_printed() == "x") && + cases.is_empty())); + assert!(matches!( + parse_ex("match x { 1 -> 2 }"), + Ok(Expression::Match(MatchExpr{ value, cases, .. })) if + matches!(value.as_ref(), Expression::Reference(_, n) if n.as_printed() == "x") && + matches!(cases.as_slice(), [MatchCase { pattern, consequent }] if + matches!(pattern, Pattern::Constant(ConstantValue::Integer(_, iwb)) if + iwb.value == 1) && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 2)))); + assert!(matches!( + parse_ex("match x { 1 -> 2, }"), + Ok(Expression::Match(MatchExpr{ value, cases, .. })) if + matches!(value.as_ref(), Expression::Reference(_, n) if n.as_printed() == "x") && + matches!(cases.as_slice(), [MatchCase { pattern, consequent }] if + matches!(pattern, Pattern::Constant(ConstantValue::Integer(_, iwb)) if + iwb.value == 1) && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 2)))); + assert!(matches!( + parse_ex("match x { 1 -> 2, 3 -> 4 }"), + Ok(Expression::Match(MatchExpr{ value, cases, .. })) if + matches!(value.as_ref(), Expression::Reference(_, n) if n.as_printed() == "x") && + matches!(cases.as_slice(), [mcase1, mcase2] if + matches!(mcase1, MatchCase { pattern, consequent } if + matches!(pattern, Pattern::Constant(ConstantValue::Integer(_, iwb)) if + iwb.value == 1) && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 2)) && + matches!(mcase2, MatchCase { pattern, consequent } if + matches!(pattern, Pattern::Constant(ConstantValue::Integer(_, iwb)) if + iwb.value == 3) && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 4))))); + assert!(matches!( + parse_ex("match x { y -> 2, }"), + Ok(Expression::Match(MatchExpr{ value, cases, .. })) if + matches!(value.as_ref(), Expression::Reference(_, n) if n.as_printed() == "x") && + matches!(cases.as_slice(), [MatchCase { pattern, consequent }] if + matches!(pattern, Pattern::Variable(n) if n.as_printed() == "y") && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 2)))); + assert!(matches!( + parse_ex("match x { Option::None -> 2, Option::Some(x) -> 4 }"), + Ok(Expression::Match(MatchExpr{ value, cases, .. })) if + matches!(value.as_ref(), Expression::Reference(_, n) if n.as_printed() == "x") && + matches!(cases.as_slice(), [mcase1, mcase2] if + matches!(mcase1, MatchCase { pattern, consequent } if + matches!(pattern, Pattern::EnumerationValue(ep) if + matches!(ep, EnumerationPattern{ type_name, variant_name, argument: None, .. } if + type_name.as_printed() == "Option" && + variant_name.as_printed() == "None")) && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 2)) && + matches!(mcase2, MatchCase { pattern, consequent } if + matches!(pattern, Pattern::EnumerationValue(ep) if + matches!(ep, EnumerationPattern{ type_name, variant_name, argument: Some(subp), .. } if + type_name.as_printed() == "Option" && + variant_name.as_printed() == "Some" && + matches!(subp.as_ref(), Pattern::Variable(n) if n.as_printed() == "x"))) && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 4))))); + assert!(matches!( + parse_ex("match x { Foo{ a, b: 2, c: d } -> 6 }"), + Ok(Expression::Match(MatchExpr{ value, cases, .. })) if + matches!(value.as_ref(), Expression::Reference(_, n) if n.as_printed() == "x") && + matches!(cases.as_slice(), [MatchCase{ pattern, consequent }] if + matches!(pattern, Pattern::Structure(StructurePattern{ type_name, fields, .. }) if + type_name.as_printed() == "Foo" && + matches!(fields.as_slice(), [(field1, None), (field2, Some(pat2)), (field3, Some(pat3))] if + field1.as_printed() == "a" && + field2.as_printed() == "b" && + field3.as_printed() == "c" && + matches!(pat2, Pattern::Constant(ConstantValue::Integer(_, iwb)) if iwb.value == 2) && + matches!(pat3, Pattern::Variable(n) if n.as_printed() == "d"))) && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 6)))); + assert!(matches!( + parse_ex("match x { Foo{ a, b: 2, c } -> 6 }"), + Ok(Expression::Match(MatchExpr{ value, cases, .. })) if + matches!(value.as_ref(), Expression::Reference(_, n) if n.as_printed() == "x") && + matches!(cases.as_slice(), [MatchCase{ pattern, consequent }] if + matches!(pattern, Pattern::Structure(StructurePattern{ type_name, fields, .. }) if + type_name.as_printed() == "Foo" && + matches!(fields.as_slice(), [(field1, None), (field2, Some(pat2)), (field3, None)] if + field1.as_printed() == "a" && + field2.as_printed() == "b" && + field3.as_printed() == "c" && + matches!(pat2, Pattern::Constant(ConstantValue::Integer(_, iwb)) if iwb.value == 2))) && + matches!(consequent, Expression::Value(ConstantValue::Integer(_, iwb)) if + iwb.value == 6)))); + + assert!(parse_ex("match x { Foo -> 3 }").is_err()); + assert!(parse_ex("match x { (4) -> 3 }").is_err()); + assert!(parse_ex("match x { Foo{ 3, x } -> 3 }").is_err()); + assert!(parse_ex("match x { Foo{ x").is_err()); + assert!(parse_ex("match x { Foo{ x: 3").is_err()); + assert!(parse_ex("match x { Foo{ x:: 3").is_err()); + assert!(parse_ex("match x { Foo{ x: 3 4 } -> 4 }").is_err()); +} diff --git a/src/syntax/print.rs b/src/syntax/print.rs new file mode 100644 index 0000000..71a0242 --- /dev/null +++ b/src/syntax/print.rs @@ -0,0 +1,70 @@ +use crate::syntax::ast::{ConstantValue, Type}; +#[cfg(test)] +use crate::syntax::parse::Parser; +#[cfg(test)] +use crate::syntax::tokens::Lexer; +use pretty::{DocAllocator, Pretty}; + +impl<'a, D: ?Sized + DocAllocator<'a, A>, A: 'a> Pretty<'a, D, A> for Type { + fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> { + match self { + Type::Constructor(_, n) => allocator.as_string(n), + Type::Variable(_, n) => allocator.as_string(n), + Type::Primitive(_, n) => allocator.text("prim%").append(allocator.as_string(n)), + + Type::Application(c, args) => c + .pretty(allocator) + .append(allocator.space()) + .append(allocator.intersperse(args, " ")), + + Type::Function(args, ret) => allocator + .intersperse(args, " ") + .append(allocator.space()) + .append(ret.pretty(allocator)), + } + } +} + +impl<'a, D: ?Sized + DocAllocator<'a, A>, A: 'a> Pretty<'a, D, A> for ConstantValue { + fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> { + match self { + ConstantValue::String(_, x) => allocator.text(format!("{x:?}")), + ConstantValue::Character(_, c) => allocator.text(format!("{c:?}")), + ConstantValue::Integer(_, iwb) => match iwb.base { + None => allocator.as_string(iwb.value), + Some(2) => allocator.text(format!("0b{:b}", iwb.value)), + Some(8) => allocator.text(format!("0o{:o}", iwb.value)), + Some(10) => allocator.text(format!("0d{}", iwb.value)), + Some(16) => allocator.text(format!("0x{:x}", iwb.value)), + Some(x) => panic!("Illegal base {x} for integer constant."), + }, + } + } +} + +proptest::proptest! { + #[test] + fn constants(x: ConstantValue) { + let allocator: pretty::Arena = pretty::Arena::new(); + let docbuilder = x.clone().pretty(&allocator); + let mut string_version = String::new(); + docbuilder.render_fmt(80, &mut string_version).expect("can render to string"); + let lexer = Lexer::from(string_version.as_str()); + let mut parser = Parser::new("test", lexer); + let roundtripped = parser.parse_constant().expect("can parse constant"); + proptest::prop_assert_eq!(x, roundtripped); + } + +// #[test] +// fn types(x: Type) { +// let allocator: pretty::Arena = pretty::Arena::new(); +// let docbuilder = x.clone().pretty(&allocator); +// let mut string_version = String::new(); +// docbuilder.render_fmt(80, &mut string_version).expect("can render to string"); +// println!("String version: {string_version:?}"); +// let lexer = Lexer::from(string_version.as_str()); +// let mut parser = Parser::new("test", lexer); +// let roundtripped = parser.parse_type().expect("can parse constant"); +// proptest::prop_assert_eq!(x, roundtripped); +// } +} diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index 2d245f7..ad096c3 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -642,6 +642,13 @@ fn numbers_work_as_expected() { }), parsed_single_token("0o10") ); + assert_eq!( + Token::Integer(IntegerWithBase { + base: None, + value: 10 + }), + parsed_single_token("0010") + ); assert_eq!( Token::Integer(IntegerWithBase { base: Some(10), @@ -681,6 +688,20 @@ fn values_work_as_expected() { assert_eq!(Token::ValueName("ɑ".into()), parsed_single_token("ɑ")); } +#[test] +fn primitives() { + assert_eq!( + Token::PrimitiveValueName("add_u8".into()), + parsed_single_token("prim%add_u8"), + ); + assert_eq!( + Token::PrimitiveTypeName("U8".into()), + parsed_single_token("prim%U8"), + ); + assert!(Lexer::from("prim%").next().unwrap().is_err()); + assert!(Lexer::from("prim%%").next().unwrap().is_err()); +} + #[test] fn operators_work_as_expected() { assert_eq!(Token::OperatorName("-".into()), parsed_single_token("-")); @@ -743,10 +764,119 @@ fn unicode() { let mut next_token = move || lexer.next().map(|x| x.expect("Can read valid token").token); assert_eq!(Some(Token::Character('¾')), next_token()); - let mut lexer = Lexer::from("'\\u{111111111111}'"); + let mut lexer = Lexer::from("'\\u{11111111111111111111111111111}'"); assert!(lexer.next().unwrap().is_err()); let mut lexer = Lexer::from("'\\u{00BE'"); assert!(lexer.next().unwrap().is_err()); let mut lexer = Lexer::from("'\\u00BE}'"); assert!(lexer.next().unwrap().is_err()); + let mut lexer = Lexer::from("'\\u"); + assert!(lexer.next().unwrap().is_err()); + let mut lexer = Lexer::from("'\\u{00Z}'"); + assert!(lexer.next().unwrap().is_err()); +} + +#[test] +fn character_string_errors() { + let mut lexer = Lexer::from("'"); + assert!(lexer.next().unwrap().is_err()); + let mut lexer = Lexer::from("'-\\"); + assert!(lexer.next().unwrap().is_err()); + let mut lexer = Lexer::from("''"); + assert!(lexer.next().unwrap().is_err()); + let mut lexer = Lexer::from("'ab'"); + assert!(lexer.next().unwrap().is_err()); + let mut lexer = Lexer::from("'\\x'"); + assert!(lexer.next().unwrap().is_err()); + let mut lexer = Lexer::from("'a'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character('a'), + .. + })) + )); + let mut lexer = Lexer::from("'\\0'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character('\0'), + .. + })) + )); + let mut lexer = Lexer::from("'\\a'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character(_), + .. + })) + )); + let mut lexer = Lexer::from("'\\b'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character(_), + .. + })) + )); + let mut lexer = Lexer::from("'\\f'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character(_), + .. + })) + )); + let mut lexer = Lexer::from("'\\n'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character(_), + .. + })) + )); + let mut lexer = Lexer::from("'\\r'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character(_), + .. + })) + )); + let mut lexer = Lexer::from("'\\t'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character(_), + .. + })) + )); + let mut lexer = Lexer::from("'\\v'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character(_), + .. + })) + )); + let mut lexer = Lexer::from("'\\''"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character('\''), + .. + })) + )); + let mut lexer = Lexer::from("'\\\\'"); + assert!(matches!( + lexer.next(), + Some(Ok(LocatedToken { + token: Token::Character('\\'), + .. + })) + )); + + let mut lexer = Lexer::from("\"foo"); + assert!(lexer.next().unwrap().is_err()); }