diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0e612ef --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1013 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "basiclings" +version = "0.1.0" +dependencies = [ + "deku", + "fancy-regex", + "fuzzy-matcher", + "inquire", + "markdown", + "rust-embed", + "serde", + "serde_json", + "tempfile", + "tifloats", + "titokens", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deku" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9711031e209dc1306d66985363b4397d4c7b911597580340b93c9729b55f6eb" +dependencies = [ + "bitvec", + "deku_derive", + "no_std_io2", + "rustversion", +] + +[[package]] +name = "deku_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cb0719583cbe4e81fb40434ace2f0d22ccc3e39a74bb3796c22b451b4f139d" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.6.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "tempfile", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "markdown" +version = "1.0.0-alpha.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6491e6c702bf7e3b24e769d800746d5f2c06a6c6a2db7992612e0f429029e81" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "no_std_io2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tifloats" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61b5d3777462924464cecb69a4961923751208adefea5241c0d2545fad9ead7" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "titokens" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e65b7e0769678ee5dfaa04d1468e816dba4ed7e3a703176632be4b738aed974" +dependencies = [ + "lazy_static", + "quick-xml", + "radix_trie", + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-id" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.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", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f5bb5257f2407a5425c6e749bfd9692192a73e70a6060516ac04f889087d68" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cce9007 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "basiclings" +version = "0.1.0" +edition = "2021" + +[dependencies] +deku = "0.18.1" +fancy-regex = "0.14.0" +fuzzy-matcher = "0.3.7" +inquire = { version = "0.7.5", features = ["editor"] } +markdown = "1.0.0-alpha.21" +rust-embed = "8.4.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tempfile = "3" +tifloats = "2" +titokens = "0.2.1" + +[features] +debug-tools = [] diff --git a/lessons/0000-welcome.md b/lessons/0000-welcome.md new file mode 100644 index 0000000..2aa6a65 --- /dev/null +++ b/lessons/0000-welcome.md @@ -0,0 +1,29 @@ +# Welcome to BASIClings +Welcome! I presume you are here to learn advanced TI-BASIC optimization. You will do this through a series of optimization puzzles, each mirroring a real-world scenario. In each lesson, you'll be presented with an unoptimized program, and you must reduce the program's byte count past a certain threshold to proceed. Along the way, we will cover underutilized techniques and solidify the strategies you probably already know. + +This course is relatively straightforward at the outset, but the difficulty will increase rapidly. The purpose of this is not to discourage you! You have the freedom to solve our problems however you'd like. Still, you'll get more out of BASIClings by solving the problems alone, without the use of external help (besides a calculator, of course). + +Sometimes, if your program does not exactly match one of my solutions, it will be subjected to behavior testing through CEmu. This course emphasizes programming for the TI-84+CE and covers tokens available only on that calculator. + +Feel free to contact me (iPhoenix) if you find yourself struggling too much with any particular problem; there's a fair chance I can use your experiences to improve the course! + +## Note: a prompt to open a text editor should show now. When you save the file, your program will be tested. + +```json +{ + "id": 0, + "name": "Welcome!", + "requirements": [], + "starting_program": "\"HELLO WORLD\"", + "required_savings": 1, + "brief_description": "Return \"HELLO WORLD\" in Ans.", + "tests": [ + { + "input": [], + "output": [ + {"name": "Ans", "value": "HELLO WORLD"} + ] + } + ] +} +``` \ No newline at end of file diff --git a/src/cemu.rs b/src/cemu.rs new file mode 100644 index 0000000..f1914f7 --- /dev/null +++ b/src/cemu.rs @@ -0,0 +1,451 @@ +use std::{ + collections::HashMap, + env, + fmt::Display, + fs, io, + path::{Path, PathBuf}, + process::{Command, ExitStatus, Stdio}, +}; + +use deku::prelude::*; +use fancy_regex::Regex; +use serde::Serialize; +use titokens::Tokens; + +use crate::{ + lesson::{Test, Variable, VariableData}, + tools::{float_to_tifloat, tokenize, tokenizer}, +}; + +#[derive(Debug)] +pub enum TestError { + Io(io::Error), + NoRom, + TIFileParsing(deku::DekuError), + CEmuCrashed(ExitStatus), + Regex(Box), +} + +impl Display for TestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TestError::Io(error) => writeln!(f, "Error occurred during tests: {}", error), + TestError::NoRom => f.write_str("Please ensure there is a working TI84+CE rom file in the current directory. This will be used for testing your submissions.\nThe rom file must end with the file extension \".rom\".\n\nThere are many ways to obtain a rom image if you do not have one. Perhaps the easiest is to use CEmu's rom dump wizard."), + TestError::TIFileParsing(deku_error) => writeln!(f, "Error parsing 8x file during tests:\n{}", deku_error), + TestError::CEmuCrashed(exit_status) => writeln!(f, "CEmu crashed during tests: {}", exit_status), + TestError::Regex(error) => writeln!(f, "Error parsing test regex: {}", error), + } + } +} + +#[derive(Debug)] +pub enum ProgramTestResult { + Pass, + // a little more information about *what* failed + Fail(String), +} + +#[derive(Clone, Serialize)] +struct Program { + name: String, + #[serde(rename = "isASM")] + is_asm: bool, +} + +#[derive(Debug, DekuRead, DekuWrite, Eq)] +#[deku(endian = "little")] +pub struct TIEntry { + #[deku(assert = "*flash_indicator == 0x0b || *flash_indicator == 0x0d")] + flash_indicator: u16, + #[deku(update = "self.data.len()")] + var_data_length: u16, + file_type: u8, + pub name: [u8; 8], + version: u8, + flags: u8, + #[deku(update = "self.data.len()")] + var_data_length_2: u16, + #[deku(count = "var_data_length_2")] + data: Vec, +} + +impl TIEntry { + fn new(name: [u8; 8], file_type: u8, data: Vec) -> Self { + TIEntry { + flash_indicator: 0x0d, + var_data_length: data.len() as u16, + file_type, + name, + version: 0, + flags: 0x00, + var_data_length_2: data.len() as u16, + data, + } + } + + fn size(&self) -> u16 { + self.to_bytes().unwrap().len() as u16 + } + + fn checksum(&self) -> u16 { + let bytes = self.to_bytes().unwrap(); + bytes.iter().fold(0, |a, &x| a.wrapping_add(x as u16)) + } +} + +impl PartialEq for TIEntry { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.data == other.data + } +} + +impl From for TIEntry { + fn from(value: Variable) -> Self { + let mut name = [0u8; 8]; + + let name_tokens: Vec = >>::into(tokenize(&value.name)) + .into_iter() + .chain(std::iter::repeat(0u8)) + .take(8) + .collect(); + name[..name_tokens.len()].copy_from_slice(&name_tokens); + + let (file_type, data): (u8, Vec) = match value.value { + VariableData::String(token_text) => { + let mut token_bytes: Vec = tokenize(&token_text).into(); + let mut len = (token_bytes.len() as u16).to_le_bytes().to_vec(); + len.append(&mut token_bytes); + + (0x04, len) + } + + VariableData::RealList(list) => ( + 0x01, + (list.len() as u16) + .to_le_bytes() + .into_iter() + .chain( + list.into_iter() + .flat_map(|element| float_to_tifloat(element).to_raw_bytes()), + ) + .collect::>(), + ), + + VariableData::RealNumber(number) => { + let data = float_to_tifloat(number).to_raw_bytes(); + (0x00, data.to_vec()) + } + }; + + TIEntry::new(name, file_type, data) + } +} + +impl From for TIEntry { + fn from(value: Tokens) -> Self { + let mut token_bytes: Vec = value.into(); + let mut len = (token_bytes.len() as u16).to_le_bytes().to_vec(); + len.append(&mut token_bytes); + + TIEntry::new(*b"TESTPROG", 0x05, len) + } +} + +// this is good enough for exactly my use here but is by no means good enough for general-purpose TI-File parsing. +#[derive(Debug, DekuRead, DekuWrite)] +#[deku(magic = b"**TI83F*\x1A\x0A")] +pub struct TIFile { + product_id: u8, + comment: [u8; 42], + #[deku(update = "self.entry.size()")] + data_length: u16, + pub entry: TIEntry, + #[deku(update = "self.entry.checksum()")] + checksum: u16, +} + +impl From for TIFile { + fn from(value: TIEntry) -> Self { + Self { + product_id: 0x00, + comment: *b"Generated for BASIClings automated testing", + data_length: value.size(), + checksum: value.checksum(), + + entry: value, + } + } +} + +#[derive(Clone, Serialize)] +pub struct AutotesterConfig { + rom: String, + target: Program, + sequence: Vec, + transfer_files: Vec, + hashes: HashMap, +} + +impl AutotesterConfig { + pub fn with_rom(rom_path: String) -> Self { + Self { + rom: rom_path, + target: Program { + name: "TESTPROG".to_owned(), + is_asm: false, + }, + sequence: vec![ + "action|launch".to_owned(), + "delay|2000".to_owned(), + "key|on".to_owned(), + ], + transfer_files: vec![], + hashes: HashMap::new(), + } + } + + pub fn add_import(&mut self, path: String) { + self.transfer_files.push(path) + } + + pub fn add_export(&mut self, var_name: &str) { + self.sequence + .push(format!("saveVar|{}", translate_variable_name(var_name))); + } +} + +/// translate from the token sheets' accessible name into CEmu's preferred name for the variable. +fn translate_variable_name(var_name: &str) -> &str { + match var_name { + "L1" => "L\u{2081}", + "L2" => "L\u{2082}", + "L3" => "L\u{2083}", + "L4" => "L\u{2084}", + "L5" => "L\u{2085}", + "L6" => "L\u{2086}", + + "theta" => "\u{03B8}", + + name => name, + } +} + +pub struct TestRunner { + rom_path: Option, +} + +impl TestRunner { + pub fn new() -> Self { + TestRunner { rom_path: None } + } + + pub fn find_rom(&mut self) -> Result { + if let Some(pathbuf) = &self.rom_path { + if !fs::exists(pathbuf).map_err(TestError::Io)? { + self.rom_path = None; + } else { + return Ok(pathbuf.to_str().unwrap().to_owned()); + } + } + + let paths = + fs::read_dir(env::current_dir().map_err(TestError::Io)?).map_err(TestError::Io)?; + + for entry in paths { + let entry = entry.map_err(TestError::Io)?; + if entry.file_name().to_string_lossy().ends_with(".rom") { + self.rom_path = Some(entry.path().canonicalize().map_err(TestError::Io)?); + return Ok(self.rom_path.as_ref().unwrap().to_str().unwrap().to_owned()); + } + } + + Err(TestError::NoRom) + } + + fn run_cemu_test( + &mut self, + program: Tokens, + inputs: &Vec, + outputs: &Vec, + ) -> Result { + let folder = tempfile::tempdir().map_err(TestError::Io)?; + let folder_path = folder.path(); + + let autotester_config_path = self.initialize_cemu_test( + &folder_path.canonicalize().map_err(TestError::Io)?, + program, + inputs, + outputs, + )?; + + let autotester_path = if cfg!(debug_assertions) { + env::current_dir() + } else { + env::current_exe() + } + .map_err(TestError::Io)? + .join("autotester"); + let cemu_status = Command::new(autotester_path) + .arg(&autotester_config_path) + .current_dir(folder_path) + .stdout(Stdio::null()) + .spawn() + .expect("Failed to start CEmu process.") + .wait() + .map_err(TestError::Io)?; + + self.validate_cemu_test_state( + cemu_status, + &folder_path.canonicalize().map_err(TestError::Io)?, + outputs, + ) + } + + /// Sets up variables, autotester config, etc + fn initialize_cemu_test( + &mut self, + folder: &Path, + program: Tokens, + inputs: &Vec, + outputs: &Vec, + ) -> Result { + let autotester_config_path = folder.join("autotester.json"); + let mut autotester_config = AutotesterConfig::with_rom(self.find_rom()?); + for input in inputs { + let destination_path = + folder.join(input.name.clone() + "." + input.value.file_extension()); + + autotester_config.add_import(destination_path.to_str().unwrap().to_owned()); + + let entry: TIEntry = (*input).clone().into(); + let file: TIFile = entry.into(); + + fs::write( + destination_path, + file.to_bytes().map_err(TestError::TIFileParsing)?, + ) + .map_err(TestError::Io)?; + } + + for output in outputs { + autotester_config.add_export(&output.name); + } + + let program_path = folder.join("TESTPROG.8xp"); + autotester_config.add_import(program_path.to_str().unwrap().to_owned()); + let program_file = fs::File::create(program_path).map_err(TestError::Io)?; + let entry: TIEntry = program.into(); + let file: TIFile = entry.into(); + file.to_writer(&mut Writer::new(program_file), ()) + .map_err(TestError::TIFileParsing)?; + + serde_json::to_writer( + fs::File::create(&autotester_config_path).map_err(TestError::Io)?, + &autotester_config, + ) + .unwrap(); + + Ok(autotester_config_path) + } + + fn validate_cemu_test_state( + &self, + cemu_status: ExitStatus, + folder: &Path, + outputs: &Vec, + ) -> Result { + if !cemu_status.success() { + Err(TestError::CEmuCrashed(cemu_status)) + } else { + for output in outputs { + let variable_name = output.name.clone(); + + let actual_path = folder.join(format!( + "{}.{}", + translate_variable_name(&variable_name), + output.value.file_extension() + )); + + if !fs::exists(&actual_path).map_err(TestError::Io)? { + if output.name == "Ans" { + return Ok(ProgramTestResult::Fail("Cannot find Ans; perhaps Ans is the wrong type at the end of your program.".to_owned())); + } + + return Ok(ProgramTestResult::Fail(format!( + "Cannot find variable {}.", + variable_name + ))); + } + + let actual = TIFile::from_reader(( + &mut fs::File::options() + .read(true) + .open(actual_path) + .map_err(TestError::Io)?, + 0, + )) + .map_err(TestError::TIFileParsing)? + .1 + .entry; + + let expected: TIEntry = output.clone().into(); + + if expected.data != actual.data { + return Ok(ProgramTestResult::Fail(format!( + "Incorrect value for variable {}.", + variable_name + ))); + } + } + + Ok(ProgramTestResult::Pass) + } + } + + /// Test program. + /// + /// If any of the root tests pass, the program passes. + /// All of the tests in a test group must pass for the whole group to pass. + pub fn run_tests( + &mut self, + program: Tokens, + tests: &Vec, + ) -> Result { + let mut last_result = ProgramTestResult::Fail("Tests failed.".to_owned()); + for test in tests { + last_result = self.run_test(program.clone(), test)?; + if let ProgramTestResult::Pass = last_result { + return Ok(ProgramTestResult::Pass); + } + } + + Ok(last_result) + } + + fn run_test(&mut self, program: Tokens, test: &Test) -> Result { + match test { + Test::CEmu { input, output } => self.run_cemu_test(program, input, output), + Test::FulltextMatch { regex } => { + let regex_result = Regex::new(&("^".to_owned() + regex + "$")) + .unwrap() + .is_match(&program.to_string(tokenizer())) + .map_err(|err| TestError::Regex(Box::new(err)))?; + + if regex_result { + Ok(ProgramTestResult::Pass) + } else { + Ok(ProgramTestResult::Fail("Tests failed.".to_owned())) + } + } + Test::Group(group) => { + for test in group { + if let ProgramTestResult::Fail(reason) = self.run_test(program.clone(), test)? { + return Ok(ProgramTestResult::Fail(reason)); + } + } + + Ok(ProgramTestResult::Pass) + } + } + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..8e791b2 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,402 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, io, +}; + +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use inquire::{ + validator::{StringValidator, Validation}, + Autocomplete, Confirm, CustomUserError, Editor, Select, Text, +}; +use markdown::mdast::{Code, Node}; +use serde::{Deserialize, Serialize}; + +use crate::{ + cemu::{ProgramTestResult, TestRunner}, + lesson::Lesson, + parser::parse_lessons, + tools::{byte_count, process_submission}, +}; + +const SAVE_PATH: &str = "basiclings_save.json"; +const RECOVERY_PATH: &str = "basiclings_recovery.json"; + +const COMMANDS: [&str; 7] = [ + "help", "select", "next", "retry", "quit", "progress", "review", +]; + +pub struct UserInterface { + lessons: BTreeMap, + save: Save, + test_runner: TestRunner, + + last_attempt: Option, +} + +impl UserInterface { + pub fn new() -> io::Result { + let mut interface = UserInterface { + lessons: parse_lessons(), + save: Save::load()?, + test_runner: TestRunner::new(), + + last_attempt: None, + }; + + interface.save(); + + Ok(interface) + } + + pub fn run(&mut self) { + self.show_progress_report(); + + loop { + let command = Text::new("") + .with_validator(MainPrompt) + .with_autocomplete(MainPrompt) + .prompt() + .unwrap(); + + match command.as_str() { + "help" => Self::show_help(), + + "select" => { + if let Some(next_lesson_id) = self.select_lesson(&self.save.unlocked_lessons) { + self.execute_lesson(next_lesson_id); + } else { + eprintln!("Operation failed.") + } + } + "next" => { + if let Some(&next_lesson_id) = self.save.unlocked_lessons.first() { + self.execute_lesson(next_lesson_id); + } else { + println!("No lessons remaining!"); + } + } + "retry" => { + if let Some(last_lesson_id) = self.last_attempt { + self.execute_lesson(last_lesson_id); + } else { + println!("No lesson to retry.") + } + } + + "quit" => break, + "progress" => self.show_progress_report(), + "review" => { + if let Some(next_lesson_id) = self.select_lesson(&self.save.completed_lessons) { + todo!() + } else { + eprintln!("Operation failed.") + } + } + _ => unreachable!(), + } + } + } + + fn lesson_markdown(&self, current_lesson: u16) -> &Node { + &self.lessons.get(¤t_lesson).unwrap().0 + } + + fn lesson_data(&self, current_lesson: u16) -> &Lesson { + &self.lessons.get(¤t_lesson).unwrap().1 + } + + fn save(&mut self) { + self.save.save().unwrap() + } + + fn show_progress_report(&self) { + let total = self.lessons.len(); + let unlocked = self.save.unlocked_lessons.len(); + let completed = self.save.completed_lessons.len(); + + println!( + "{:.2}% complete: {} completed / {} unlocked / {} unseen", + 100.0 * completed as f64 / total as f64, + completed, + unlocked, + total - completed - unlocked, + ); + } + + fn show_help() { + println!("Welcome to BASIClings."); + } + + fn print_lesson(&self, lesson_id: u16) { + UserInterface::print_node(self.lesson_markdown(lesson_id)); + } + + fn print_node(node: &Node) { + // does not support nested styling tags... we shouldn't need it? + match node { + Node::Text(text) => print!("{}", text.value), + Node::InlineCode(inline_code) => print!("{}", inline_code.value), + + Node::Code(Code { + lang: Some(lang), .. + }) if lang == "json" => return, + _ => {} + } + + if let Some(children) = node.children() { + children.iter().for_each(UserInterface::print_node); + } + + if matches!(node, Node::Heading(_) | Node::Paragraph(_)) { + println!("\n") + } + } + + fn execute_lesson(&mut self, lesson_id: u16) { + self.print_lesson(lesson_id); + + let lesson_data = &self.lessons.get(&lesson_id).unwrap().1; + + let savings_message = format!( + "Save {} byte{} to proceed. (target: {} bytes)", + lesson_data.required_savings, + if lesson_data.required_savings > 1 { + "s" + } else { + "" + }, + lesson_data.byte_threshold() + ); + + let boilerplate = self + .save + .attempts + .get(&lesson_id) + .cloned() + .unwrap_or_else(|| { + let objective = lesson_data + .brief_description + .clone() + .map_or("".to_owned(), |objective| { + "\n// Objective: ".to_owned() + &objective.replace("\n", "\n// ") + }); + "// Blank lines and lines starting with two forward slashes are ignored.\n" + .to_owned() + + &objective + + "\n// Original Program:\n// " + + &lesson_data.starting_program.replace("\n", "\n// ") + + "\n\n// " + + &savings_message + + "\n\n" + + &lesson_data.starting_program + }); + + let result = Editor::new(&savings_message) + .with_predefined_text(&boilerplate) + .with_file_extension(".8xp.txt") + .prompt(); + + if let Ok(raw_text) = result { + if raw_text.trim() == "" { + return; + } + + self.save.attempts.insert(lesson_id, raw_text.clone()); + + let tokens_struct = process_submission(raw_text); + let tokens = tokens_struct.clone().collect::>(); + let byte_count: usize = byte_count(&tokens); + + println!("{} tokens, {} bytes.", tokens.len(), byte_count); + + let byte_threshold = lesson_data.byte_threshold(); + if byte_count > byte_threshold { + println!("Too large: target is {} bytes", byte_threshold); + self.last_attempt = Some(lesson_id); + } else { + println!("Testing..."); + match self + .test_runner + .run_tests(tokens_struct, &lesson_data.tests) + { + Err(test_error) => { + eprintln!("{}", test_error); + std::process::exit(1) + } + + Ok(ProgramTestResult::Fail(reason)) => { + println!("{}", reason); + self.last_attempt = Some(lesson_id); + } + Ok(ProgramTestResult::Pass) => { + self.complete_lesson(lesson_id); + self.last_attempt = None; + } + } + } + + self.save(); + } + } + + fn complete_lesson(&mut self, lesson_id: u16) { + self.save.unlocked_lessons.remove(&lesson_id); + self.save.completed_lessons.insert(lesson_id); + + // we could precompute this but it's not really necessary + // when there are only a couple hundred lessons at most. + let new_lessons = self + .lessons + .iter() + .filter_map(|(&k, (_, v))| { + if !self.save.completed_lessons.contains(&k) + && !self.save.unlocked_lessons.contains(&k) + && !v + .requirements + .iter() + .any(|x| !self.save.completed_lessons.contains(x)) + { + Some(k) + } else { + None + } + }) + .collect::>(); + + if !new_lessons.is_empty() { + println!( + "Congratulations! You unlocked {} new lesson{}:", + new_lessons.len(), + if new_lessons.len() > 1 { "s" } else { "" } + ); + + new_lessons + .iter() + .for_each(|&lesson_id| println!(" - {}", self.lesson_data(lesson_id).name)); + } else { + println!("Congratulations!") + } + + self.save.unlocked_lessons.extend(new_lessons.iter()); + self.show_progress_report(); + } + + fn select_lesson(&self, set: &BTreeSet) -> Option { + if set.len() == 1 { + let lesson_id = *set.first().unwrap(); + let proceed = Confirm::new(&format!( + "Only one lesson available: {}. Would you like to select it?", + self.lesson_data(lesson_id).name + )) + .with_default(true) + .prompt() + .ok()?; + + if proceed { + return Some(lesson_id); + } else { + return None; + } + } else { + println!("{} lessons available.", set.len()) + } + + let selection = Select::new( + "Select a lesson:", + set.iter() + .map(|&x| self.lesson_data(x).name.clone()) + .collect(), + ) + .prompt() + .ok()?; + + set.iter() + .find(|&&x| self.lesson_data(x).name == selection) + .copied() + } +} + +#[derive(Serialize, Deserialize)] +pub struct Save { + pub unlocked_lessons: BTreeSet, + pub completed_lessons: BTreeSet, + + pub attempts: BTreeMap, +} + +impl Save { + pub fn load() -> Result { + if !fs::exists(SAVE_PATH)? { + return Ok(Save::default()); + } + + let data = fs::read_to_string(SAVE_PATH)?; + + let save: Save = serde_json::from_str(&data).unwrap_or_else(|_err| { + fs::write(RECOVERY_PATH, data).unwrap(); + eprintln!("There was an error recovering your save. A copy of the malformed save file was written to {}, and a new save was created.", RECOVERY_PATH); + + Save::default() + }); + + Ok(save) + } + + pub fn save(&self) -> Result<(), std::io::Error> { + fs::write(SAVE_PATH, serde_json::to_string(&self).unwrap()) + } +} + +impl Default for Save { + fn default() -> Self { + Save { + unlocked_lessons: BTreeSet::from([0]), + completed_lessons: BTreeSet::new(), + + attempts: BTreeMap::new(), + } + } +} + +#[derive(Clone)] +pub struct MainPrompt; + +impl Autocomplete for MainPrompt { + fn get_suggestions(&mut self, input: &str) -> Result, inquire::CustomUserError> { + let mut matches: Vec<(String, i64)> = COMMANDS + .into_iter() + .filter_map(|command| { + SkimMatcherV2::default() + .ignore_case() + .fuzzy_match(command, input) + .map(|score| (command.to_string(), score)) + }) + .collect(); + + matches.sort_by(|a, b| b.1.cmp(&a.1)); + Ok(matches.into_iter().map(|x| x.0).collect()) + } + + fn get_completion( + &mut self, + input: &str, + highlighted_suggestion: Option, + ) -> Result { + let first_suggestion = self + .get_suggestions(input) + .ok() + .and_then(|x| x.first().cloned()); + + Ok(highlighted_suggestion.or(first_suggestion)) + } +} + +impl StringValidator for MainPrompt { + fn validate(&self, input: &str) -> Result { + if COMMANDS.contains(&input) { + Ok(Validation::Valid) + } else { + Ok(Validation::Invalid("Expected a valid command".into())) + } + } +} diff --git a/src/lesson.rs b/src/lesson.rs new file mode 100644 index 0000000..16c7ac0 --- /dev/null +++ b/src/lesson.rs @@ -0,0 +1,59 @@ +use std::collections::BTreeSet; + +use serde::Deserialize; + +use crate::tools::{byte_count, tokenize}; + +#[derive(Deserialize, Debug)] +pub enum Test { + #[serde(untagged)] + CEmu { + input: Vec, + output: Vec, + }, + #[serde(untagged)] + FulltextMatch { regex: String }, + #[serde(untagged)] + Group(Vec), +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum VariableData { + String(String), + RealList(Vec), + RealNumber(f64), +} + +impl VariableData { + pub fn file_extension(&self) -> &str { + match self { + VariableData::String(_) => "8xs", + VariableData::RealList(_) => "8xl", + VariableData::RealNumber(_) => "8xn", + } + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Variable { + pub name: String, + pub value: VariableData, +} + +#[derive(Deserialize)] +pub struct Lesson { + pub id: u16, + pub name: String, + pub requirements: BTreeSet, + pub starting_program: String, + pub required_savings: usize, + pub brief_description: Option, + pub tests: Vec, +} + +impl Lesson { + pub fn byte_threshold(&self) -> usize { + byte_count(&tokenize(&self.starting_program).collect::>()) - self.required_savings + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8f93aba --- /dev/null +++ b/src/main.rs @@ -0,0 +1,13 @@ +use cli::UserInterface; + +mod cemu; +mod cli; +mod lesson; +mod parser; +mod tools; + +fn main() { + let mut cli = UserInterface::new().unwrap(); + + cli.run() +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..62512d5 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,50 @@ +use std::collections::BTreeMap; + +use markdown::{ + mdast::{Code, Node}, + ParseOptions, +}; +use rust_embed::Embed; + +use crate::lesson::Lesson; + +#[derive(Embed)] +#[folder = "lessons/"] +pub struct LessonData; + +pub fn parse_lessons() -> BTreeMap { + let mut data = BTreeMap::new(); + + for file_path in LessonData::iter() { + // Every lesson should be valid UTF8 & markdown- I don't feel so bad making the runtime errors worse than useless. + let raw_data = LessonData::get(&file_path).unwrap().data; + let lesson_data = String::from_utf8(raw_data.to_vec()).expect("UTF-8 parsing error"); + let ast = + markdown::to_mdast(&lesson_data, &ParseOptions::gfm()).expect("Markdown parsing error"); + + // extract the yaml metadata (usually at the end) + let metadata = ast.children().unwrap().iter().find_map(|node| match &node { + Node::Code(Code { + lang: Some(lang), + value, + .. + }) if lang == "json" => Some(value), + _ => None, + }); + + if metadata.is_none() { + eprintln!("Missing metadata for {}", file_path); + continue; + } + + let lesson: Result = serde_json::from_str(metadata.unwrap()); + match lesson { + Ok(lesson) => { + data.insert(lesson.id, (ast, lesson)); + } + Err(err) => panic!("Error in lesson {}:\n{}", file_path, err), + } + } + + data +} diff --git a/src/tools.rs b/src/tools.rs new file mode 100644 index 0000000..07f6a10 --- /dev/null +++ b/src/tools.rs @@ -0,0 +1,57 @@ +use std::sync::OnceLock; + +use tifloats::Float; +use titokens::{Token, Tokenizer, Tokens, Version}; + +pub fn tokenizer() -> &'static Tokenizer { + static TOKENIZER: OnceLock = OnceLock::new(); + + TOKENIZER.get_or_init(|| Tokenizer::new(Version::latest(), "en")) +} + +pub fn tokenize(text: &str) -> Tokens { + tokenizer().tokenize(text).unwrap().0 +} + +pub fn byte_count(tokens: &[Token]) -> usize { + tokens + .iter() + .map(|token| match token { + Token::OneByte(_) => 1, + Token::TwoByte(_, _) => 2, + }) + .sum() +} + +pub fn process_submission(submission: String) -> Tokens { + let filtered = submission + .lines() + .filter(|&line| !(line.is_empty() || line.starts_with("//"))) + .collect::>() + .join("\n"); + + let (tokens, _boundaries) = tokenizer().tokenize(&filtered).unwrap(); + tokens +} + +pub fn float_to_tifloat(value: f64) -> Float { + if value == 0.0 { + return Float::new_unchecked(false, 0, 0); + } + + let exponent = value.abs().log10().floor() as i8; + + let mut rescaled = (10.0f64).powi(-exponent as i32) * value.abs(); + let mut digits = Vec::with_capacity(15); + for _ in 0..15 { + digits.push(rescaled.trunc() as u8); + rescaled = rescaled.fract() * 10.0; + } + + Float::new( + value.is_sign_negative(), + exponent, + Float::mantissa_from(&digits), + ) + .unwrap() +}