diff --git a/Cargo.lock b/Cargo.lock index d6426a1..1875360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.21" @@ -58,18 +67,61 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + [[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -83,7 +135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "rand_core", ] @@ -161,6 +213,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -170,12 +231,47 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[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 = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -205,6 +301,9 @@ dependencies = [ "clap", "cliclack", "console", + "inkwell", + "lalrpop", + "lalrpop-util", "pretty_assertions", "rand", "vfs", @@ -221,12 +320,34 @@ dependencies = [ "libredox", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.4.1" @@ -293,12 +414,45 @@ dependencies = [ "web-time", ] +[[package]] +name = "inkwell" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7decbc9dfa45a4a827a6ff7b822c113b1285678a937e84213417d4ca8a095782" +dependencies = [ + "bitflags", + "inkwell_internals", + "libc", + "llvm-sys", + "thiserror", +] + +[[package]] +name = "inkwell_internals" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cfe97ee860815a90ed17e09639513269e39420a7440f3f4c996f238c514cf8d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -315,6 +469,52 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lalrpop" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a80a963123205c7157323c99611bc4abb65dcbd62ef46dc4bac74a3941bc75" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884f3e747ed2dcee867cda1b0c31a048f9e20de2d916a248949319921a2e666e" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -335,7 +535,7 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", - "redox_syscall", + "redox_syscall 0.7.1", ] [[package]] @@ -344,6 +544,29 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "llvm-sys" +version = "221.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2abcc34a3b190f03c2a61b555f218f529589ff13657bdd2ff8ac3e85f2abe6bb" +dependencies = [ + "anyhow", + "cc", + "lazy_static", + "libc", + "regex-lite", + "semver", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -356,6 +579,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "once_cell" version = "1.21.3" @@ -368,12 +597,67 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -435,6 +719,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_syscall" version = "0.7.1" @@ -444,6 +737,41 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rustix" version = "1.1.3" @@ -463,6 +791,21 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[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 = "semver" version = "1.0.27" @@ -511,12 +854,52 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "smawk" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.11.1" @@ -534,6 +917,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "terminal_size" version = "0.4.3" @@ -555,6 +947,32 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -591,6 +1009,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "vfs" version = "0.12.2" @@ -600,6 +1024,16 @@ dependencies = [ "filetime", ] +[[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 = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -707,6 +1141,15 @@ dependencies = [ "wasm-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" diff --git a/Cargo.toml b/Cargo.toml index e223f7e..5c5d34e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,12 @@ clap = { version = "4.5.58", features = ["derive", "wrap_help", "string", "cargo cliclack = "0.3.9" console = "0.16.2" vfs = "0.12.2" +lalrpop-util = "0.23.1" +inkwell = { version = "0.9.0", features = ["llvm22-1"] } [dev-dependencies] pretty_assertions = "1.4.1" rand = "0.10.0" + +[build-dependencies] +lalrpop = "0.23.1" diff --git a/src/errors/test.rs b/build.rs similarity index 66% rename from src/errors/test.rs rename to build.rs index 772d820..c2cc128 100644 --- a/src/errors/test.rs +++ b/build.rs @@ -15,21 +15,11 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use crate::errors::{GenericError, NotImplementedError}; -use pretty_assertions::assert_eq; - -#[test] -fn it_stores_message() { - assert_eq!( - "My error message", - GenericError::new("My error message").to_string() - ); -} - -#[test] -fn it_tells_feature_is_not_implemented() { - assert_eq!( - "foo is not yet implemented", - NotImplementedError::new("foo").to_string() - ); +fn main() { + lalrpop::Configuration::new() + .set_in_dir("./src") + .set_out_dir("./src") + .force_build(true) + .process() + .unwrap() } diff --git a/fil.nix b/fil.nix index ee4378d..045aec0 100644 --- a/fil.nix +++ b/fil.nix @@ -1,4 +1,11 @@ -{ rustPlatform, lib }: +{ + rustPlatform, + lib, + libllvm, + libffi, + libxml2, + zlib, +}: rustPlatform.buildRustPackage rec { name = "fil"; @@ -8,6 +15,13 @@ rustPlatform.buildRustPackage rec { lockFile = "${src}/Cargo.lock"; }; + nativeBuildInputs = [ libllvm ]; + buildInputs = [ + libffi + libxml2 + zlib + ]; + doCheck = true; checkPhase = '' runHook preCheck diff --git a/flake.lock b/flake.lock index 16a747c..31b5d54 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1769598131, - "narHash": "sha256-e7VO/kGLgRMbWtpBqdWl0uFg8Y2XWFMdz0uUJvlML8o=", + "lastModified": 1775710090, + "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fa83fd837f3098e3e678e6cf017b2b36102c7211", + "rev": "4c1018dae018162ec878d42fec712642d214fdfa", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.11", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 2ccafe5..546857b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,6 @@ { inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; treefmt-nix = { url = "github:numtide/treefmt-nix/28b19c5844cc6e2257801d43f2772a4b4c050a1b"; @@ -22,9 +22,14 @@ eachSystem = nixpkgs.lib.genAttrs (import systems); pkgs = eachSystem (system: import nixpkgs { inherit system; }); - fil-version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version; + fil-version = (fromTOML (builtins.readFile ./Cargo.toml)).package.version; - fil-package = eachSystem (system: pkgs.${system}.callPackage ./fil.nix { }); + fil-package = eachSystem ( + system: + pkgs.${system}.callPackage ./fil.nix { + libllvm = pkgs.${system}.llvmPackages_22.libllvm; + } + ); rpm-package = eachSystem ( system: pkgs.${system}.callPackage ./tools/package/rpm.nix { @@ -56,12 +61,22 @@ packages = with pkgs.${system}; [ git rustup + llvmPackages_22.libllvm + libffi + libxml2 (import ./tools/nix/treefmt.nix { inherit treefmt-nix; pkgs = pkgs.${system}; }) ]; + LD_LIBRARY_PATH = + with pkgs.${system}; + lib.makeLibraryPath [ + libffi + stdenv.cc.cc + ]; + shellHook = '' export ROOT_DIR=$(git rev-parse --show-toplevel) export PATH="$PATH:$ROOT_DIR/tools/bin" diff --git a/src/build/grammar/.gitignore b/src/build/grammar/.gitignore new file mode 100644 index 0000000..718e625 --- /dev/null +++ b/src/build/grammar/.gitignore @@ -0,0 +1 @@ +grammar.rs diff --git a/src/build/grammar/ast.rs b/src/build/grammar/ast.rs new file mode 100644 index 0000000..2422a3b --- /dev/null +++ b/src/build/grammar/ast.rs @@ -0,0 +1,50 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use std::fmt::{Debug, Formatter}; + +pub enum Expr { + Number(u32), + Op(Box, Opcode, Box), +} + +pub enum Opcode { + Mul, + Div, + Add, + Sub, +} + +impl Debug for Expr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Expr::Number(n) => write!(f, "{n:?}"), + Expr::Op(l, op, r) => write!(f, "({l:?} {op:?} {r:?})"), + } + } +} + +impl Debug for Opcode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Opcode::Mul => write!(f, "*"), + Opcode::Div => write!(f, "/"), + Opcode::Add => write!(f, "+"), + Opcode::Sub => write!(f, "-"), + } + } +} diff --git a/src/build/grammar/grammar.lalrpop b/src/build/grammar/grammar.lalrpop new file mode 100644 index 0000000..57772fe --- /dev/null +++ b/src/build/grammar/grammar.lalrpop @@ -0,0 +1,33 @@ +use std::str::FromStr; +use crate::build::grammar::ast::{Expr, Opcode}; + +grammar; + +pub Expr: Box = { + Expr ExprOp Factor => Box::new(Expr::Op(<>)), + Factor, +}; + +ExprOp: Opcode = { + "+" => Opcode::Add, + "-" => Opcode::Sub, +}; + +Factor: Box = { + Factor FactorOp Term => Box::new(Expr::Op(<>)), + Term, +}; + +FactorOp: Opcode = { + "*" => Opcode::Mul, + "/" => Opcode::Div, +}; + +Term: Box = { + Num => Box::new(Expr::Number(<>)), + "(" ")" +}; + +Num: u32 = { + r"[0-9]+" => u32::from_str(<>).unwrap() +}; diff --git a/src/build/grammar/mod.rs b/src/build/grammar/mod.rs new file mode 100644 index 0000000..c2075ab --- /dev/null +++ b/src/build/grammar/mod.rs @@ -0,0 +1,90 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use crate::build::grammar::ast::Expr; +use crate::fault; +use crate::fault::Fault; +use parse_error_formatter::format_parse_error; + +pub mod ast; +pub mod grammar; +mod parse_error_formatter; + +pub fn parse_file(main_source_file: &vfs::path::VfsPath) -> fault::Result> { + main_source_file + .read_to_string() + .map_err(|error| Fault::from_error(Box::from(error))) + .and_then(|content| { + grammar::ExprParser::new() + .parse(content.as_str()) + .map_err(|error| Fault::from_message(format_parse_error(error, &content).as_str())) + }) +} + +#[cfg(test)] +mod test { + use crate::build::grammar::grammar; + use crate::build::grammar::parse_file; + use pretty_assertions::{assert_eq, assert_str_eq}; + use vfs::{MemoryFS, VfsPath}; + + #[test] + fn test_grammar() { + let expr = grammar::ExprParser::new().parse("22 * 44 + 66").unwrap(); + assert_str_eq!(&format!("{:?}", expr), "((22 * 44) + 66)"); + } + + #[test] + fn test_parse_file() { + let root = VfsPath::new(MemoryFS::new()); + root.join("src").unwrap().create_dir().unwrap(); + let source_file = root.join("src/main.rs").unwrap(); + source_file.create_file().unwrap(); + source_file + .append_file() + .unwrap() + .write_fmt(format_args!("1 + 3 * 12 -4")) + .unwrap(); + + let expr = parse_file(&source_file).unwrap(); + assert_str_eq!(&format!("{:?}", expr), "((1 + (3 * 12)) - 4)"); + } + + #[test] + fn test_parse_file_err() { + let root = VfsPath::new(MemoryFS::new()); + root.join("src").unwrap().create_dir().unwrap(); + let source_file = root.join("src/main.rs").unwrap(); + source_file.create_file().unwrap(); + source_file + .append_file() + .unwrap() + .write_fmt(format_args!("1 + hello")) + .unwrap(); + + let result = parse_file(&source_file); + assert_eq!(result.is_err(), true); + assert_str_eq!( + format!("{}", result.err().unwrap()), + "Invalid token at line 1: + + 1 | 1 + hello + ^ +" + ); + } +} diff --git a/src/build/grammar/parse_error_formatter.rs b/src/build/grammar/parse_error_formatter.rs new file mode 100644 index 0000000..67c5317 --- /dev/null +++ b/src/build/grammar/parse_error_formatter.rs @@ -0,0 +1,386 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use lalrpop_util::ParseError; +use lalrpop_util::lexer::Token; + +pub fn format_parse_error(error: ParseError, source: &String) -> String { + let result = match error { + ParseError::InvalidToken { location } => parse_invalid_token(location, source), + ParseError::UnrecognizedEof { + location, + ref expected, + } => parse_unrecognized_eof(location, expected, source), + ParseError::UnrecognizedToken { + ref token, + ref expected, + } => parse_unrecognized_token(token, expected, source), + ParseError::ExtraToken { ref token } => parse_extra_token(token, source), + ParseError::User { error } => Some(String::from(error)), + }; + + result.unwrap_or(format!("{error}")) +} + +fn parse_invalid_token(location: usize, source: &String) -> Option { + find_line(location, location, source) + .map(|(line, n)| format!("Invalid token at line {n}:\n\n{line}")) +} + +fn parse_unrecognized_eof( + location: usize, + expected: &Vec, + source: &String, +) -> Option { + find_line(location, location, source).map(|(line, n)| { + let expected_str = format_expected(expected); + format!("Unexpected end of file (EOF) at line {n}:\n\n{line}\n{expected_str}\n") + }) +} + +fn parse_unrecognized_token( + (start, token, end): &(usize, Token, usize), + expected: &Vec, + source: &String, +) -> Option { + find_line(*start, *end, source).map(|(line, n)| { + let expected_str = format_expected(expected); + format!("Unexpected token '{token}' at line {n}:\n\n{line}\n{expected_str}\n") + }) +} + +fn parse_extra_token( + (start, token, end): &(usize, Token, usize), + source: &String, +) -> Option { + find_line(*start, *end, source) + .map(|(line, n)| format!("Extra token '{token}' found at line {n}:\n\n{line}\n")) +} + +fn format_expected(expected: &Vec) -> String { + let mut result = String::new(); + for (i, e) in expected.iter().enumerate() { + let sep = match i { + 0 => "Expected", + _ if i < expected.len() - 1 => ",", + _ => " or", + }; + result = format!("{result}{sep} '{e}'"); + } + result +} + +struct LineEntry { + line: String, + nth_line: usize, + range: (usize, usize), +} + +fn find_line(start: usize, end: usize, source: &String) -> Option<(String, usize)> { + let lines = collect_lines(start, end, source); + if lines.is_empty() { + None + } else if lines.len() == 1 { + let line = lines.first()?; + let n = line.nth_line; + let content = &line.line; + let n_spacing = " ".repeat(format!("{n}").len()); + let spacing = " ".repeat(line.range.0); + let hats = "^".repeat(line.range.1 - line.range.0 + 1); + Some(( + format!(" {n} | {content}\n {n_spacing} {spacing}{hats}\n"), + n, + )) + } else { + let first_line = lines.first()?; + let last_line = lines.last()?; + let n_len = format!("{}", last_line.nth_line).len(); + let n_spacing = " ".repeat(n_len); + let mut content = String::new(); + for (i, line) in lines.iter().enumerate() { + let n = line.nth_line; + let front_spacing = " ".repeat(n_len - format!("{n}").len()); + let line_content = &line.line; + if i == 0 { + let spacing = " ".repeat(line.range.0); + content += + format!(" {n_spacing} {spacing}v\n {front_spacing}{n} | {line_content}\n") + .as_str(); + } else if i == lines.len() - 1 { + let spacing = " ".repeat(line.range.1); + content += + format!(" {front_spacing}{n} | {line_content}\n {n_spacing} {spacing}^\n") + .as_str(); + } else { + content += format!(" {front_spacing}{n} | {line_content}\n").as_str(); + } + } + Some((content, first_line.nth_line)) + } +} + +fn collect_lines(start: usize, end: usize, source: &String) -> Vec { + let mut lines = Vec::new(); + let mut position_start = 0; + let mut position_end = 0; + let mut collect = false; + for (n, line) in source.split("\n").enumerate() { + position_end += line.len() + 1; + let mut line_start = 0; + let mut line_end = line.len(); + if start >= position_start && start < position_end { + line_start = start - position_start; + collect = true; + } + if collect { + if end >= position_start && end < position_end { + line_end = end - position_start; + } + lines.push(LineEntry { + line: String::from(line), + nth_line: n + 1, + range: (line_start, line_end), + }); + } + if end >= position_start && end < position_end { + collect = false; + } + + position_start = position_end; + } + lines +} + +#[cfg(test)] +mod test { + use crate::build::grammar::parse_error_formatter::{ + find_line, format_expected, format_parse_error, + }; + use lalrpop_util::ParseError; + use lalrpop_util::lexer::Token; + use pretty_assertions::{assert_eq, assert_str_eq}; + + #[test] + fn test_parse_invalid_token() { + assert_str_eq!( + "Invalid token at line 2: + + 2 | bar + ^ +", + format_parse_error( + ParseError::InvalidToken { location: 5 }, + &String::from("foo\nbar\nbaz") + ) + ); + assert_str_eq!( + "Invalid token at 50", + format_parse_error( + ParseError::InvalidToken { location: 50 }, + &String::from("foo\nbar\nbaz") + ) + ) + } + + #[test] + fn test_parse_unrecognized_eof() { + assert_str_eq!( + "Unexpected end of file (EOF) at line 3: + + 3 | baz + ^ + +Expected 'toto' or 'titi' +", + format_parse_error( + ParseError::UnrecognizedEof { + location: 11, + expected: vec![String::from("toto"), String::from("titi")], + }, + &String::from("foo\nbar\nbaz"), + ) + ); + } + + #[test] + fn test_parse_unrecognized_token() { + assert_str_eq!( + "Unexpected token 'hello' at line 2: + + 2 | bar + ^^ + +Expected 'toto' or 'titi' +", + format_parse_error( + ParseError::UnrecognizedToken { + token: (5, Token(0, "hello"), 6), + expected: vec![String::from("toto"), String::from("titi")], + }, + &String::from("foo\nbar\nbaz"), + ), + ); + } + + #[test] + fn test_parse_user() { + assert_str_eq!( + "factoring", + format_parse_error(ParseError::User { error: "factoring" }, &String::new(),) + ) + } + + #[test] + fn test_parse_extra_token() { + assert_str_eq!( + "Extra token 'hello' found at line 2: + + 2 | bar + ^^ + +", + format_parse_error( + ParseError::ExtraToken { + token: (5, Token(0, "hello"), 6) + }, + &String::from("foo\nbar\nbaz"), + ), + ) + } + + #[test] + fn test_format_expected() { + assert_str_eq!( + "Expected 'foo'", + format_expected(&vec![String::from("foo")]) + ); + assert_str_eq!( + "Expected 'foo' or 'bar'", + format_expected(&vec![String::from("foo"), String::from("bar")]) + ); + assert_str_eq!( + "Expected 'foo', 'bar' or 'baz'", + format_expected(&vec![ + String::from("foo"), + String::from("bar"), + String::from("baz") + ]) + ); + } + + #[test] + fn test_find_line() { + assert_eq!( + Some(( + String::from( + " 2 | bar + ^ +" + ), + 2 + )), + find_line(5, 5, &String::from("foo\nbar\nbaz")), + ); + assert_eq!( + Some(( + String::from( + " 1 | foo + ^ +" + ), + 1 + )), + find_line(0, 0, &String::from("foo\nbar\nbaz")), + ); + assert_eq!( + Some(( + String::from( + " 1 | foo + ^ +" + ), + 1 + )), + find_line(3, 3, &String::from("foo\nbar\nbaz")), + ); + assert_eq!( + Some(( + String::from( + " 3 | baz + ^ +" + ), + 3 + )), + find_line(10, 10, &String::from("foo\nbar\nbaz")), + ); + assert_eq!(None, find_line(100, 100, &String::from(""))); + } + + #[test] + fn test_find_line_range() { + assert_eq!( + Some(( + String::from( + " 2 | Hello World! + ^^^^^ +" + ), + 2 + )), + find_line(10, 14, &String::from("foo\nHello World!\nbaz")), + ); + } + + #[test] + fn test_find_line_multiline() { + assert_eq!( + Some(( + String::from( + " v + 2 | tete + 3 | titi + 4 | toto + ^ +" + ), + 2 + )), + find_line(7, 16, &String::from("tata\ntete\ntiti\ntoto\ntutu\n")), + ); + assert_eq!( + Some(( + String::from( + " v + 9 | i + 10 | j + 11 | k + ^ +" + ), + 9 + )), + find_line( + 16, + 20, + &String::from( + "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz" + ) + ), + ); + } +} diff --git a/src/build/ir/builder_error_formatter.rs b/src/build/ir/builder_error_formatter.rs new file mode 100644 index 0000000..46c8470 --- /dev/null +++ b/src/build/ir/builder_error_formatter.rs @@ -0,0 +1,81 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use inkwell::builder::BuilderError; + +pub fn format_builder_error(error: &BuilderError) -> String { + format!("{error}") +} + +#[cfg(test)] +mod test { + use crate::build::ir::builder_error_formatter::format_builder_error; + use inkwell::builder::{BuilderError, CmpxchgOrderingError}; + use inkwell::error::AlignmentError; + use inkwell::values::AtomicError; + use pretty_assertions::assert_str_eq; + + #[test] + fn test_it_returns_error_message() { + assert_str_eq!( + "Builder position is not set", + format_builder_error(&BuilderError::UnsetPosition) + ); + assert_str_eq!( + "Alignment error", + format_builder_error(&BuilderError::AlignmentError(AlignmentError::Unsized)) + ); + assert_str_eq!( + "Aggregate extract index out of range", + format_builder_error(&BuilderError::ExtractOutOfRange) + ); + assert_str_eq!( + "The bitwidth of value must be a power of 2 and greater than or equal to 8.", + format_builder_error(&BuilderError::BitwidthError) + ); + assert_str_eq!( + "Pointee type does not match the value's type", + format_builder_error(&BuilderError::PointeeTypeMismatch) + ); + assert_str_eq!( + "Values must have the same type", + format_builder_error(&BuilderError::NotSameType) + ); + assert_str_eq!( + "Values must have pointer or integer type", + format_builder_error(&BuilderError::NotPointerOrInteger) + ); + assert_str_eq!( + "Cmpxchg ordering error or mismatch", + format_builder_error(&BuilderError::CmpxchgOrdering( + CmpxchgOrderingError::WeakerThanMonotic + )) + ); + assert_str_eq!( + "Atomic ordering error", + format_builder_error(&BuilderError::AtomicOrdering(AtomicError::ReleaseOnLoad)) + ); + assert_str_eq!( + "GEP pointee is not a struct", + format_builder_error(&BuilderError::GEPPointee) + ); + assert_str_eq!( + "GEP index out of range", + format_builder_error(&BuilderError::GEPIndex) + ); + } +} diff --git a/src/build/ir/mod.rs b/src/build/ir/mod.rs new file mode 100644 index 0000000..bdcc33c --- /dev/null +++ b/src/build/ir/mod.rs @@ -0,0 +1,132 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +mod builder_error_formatter; + +use crate::build::grammar::ast::{Expr, Opcode}; +use crate::build::ir::builder_error_formatter::format_builder_error; +use crate::fault; +use crate::fault::Fault; +use inkwell::builder::Builder; +use inkwell::context::Context; +use inkwell::module::Module; +use inkwell::values::{FunctionValue, IntValue}; + +struct Compiler<'a, 'ctx> { + pub context: &'ctx Context, + pub builder: &'a Builder<'ctx>, + pub module: &'a Module<'ctx>, +} + +impl<'a, 'ctx> Compiler<'a, 'ctx> { + fn compile_expr(&mut self, expr: &Expr) -> fault::Result> { + match expr { + Expr::Number(n) => Ok(self.context.i32_type().const_int(*n as u64, false)), + Expr::Op(l, o, r) => self.compile_operator(l, o, r), + } + } + + fn compile_operator( + &mut self, + left: &Expr, + operator: &Opcode, + right: &Expr, + ) -> fault::Result> { + let lhs = self.compile_expr(left)?; + let rhs = self.compile_expr(right)?; + + match operator { + Opcode::Mul => self.builder.build_int_mul(lhs, rhs, "fil_mul"), + Opcode::Div => self.builder.build_int_unsigned_div(lhs, rhs, "fil_dib"), + Opcode::Add => self.builder.build_int_add(lhs, rhs, "fil_add"), + Opcode::Sub => self.builder.build_int_sub(lhs, rhs, "fil_sub"), + } + .map_err(|err| Fault::from_message(format_builder_error(&err).as_str())) + } + + fn entry_function(&self) -> fault::Result> { + let function_type = self.context.i32_type().fn_type(&[], false); + let function_value = self.module.add_function("main", function_type, None); + + Ok(function_value) + } + + pub fn compile( + context: &'ctx Context, + builder: &'a Builder<'ctx>, + module: &'a Module<'ctx>, + expr: &Expr, + ) -> fault::Result> { + let mut compiler = Self { + context, + builder, + module, + }; + + let function = compiler.entry_function()?; + let entry = compiler.context.append_basic_block(function, "entry"); + compiler.builder.position_at_end(entry); + + let body = compiler.compile_expr(expr)?; + + compiler + .builder + .build_return(Some(&body)) + .map_err(|err| Fault::from_error(Box::from(err)))?; + + if function.verify(true) { + Ok(function) + } else { + unsafe { + function.delete(); + } + Err(Fault::from_message("Invalid generated main function")) + } + } +} + +pub fn transform_to_ir(expr: &Expr) -> fault::Result { + let context = Context::create(); + let builder = context.create_builder(); + let module = context.create_module("fil"); + + Compiler::compile(&context, &builder, &module, expr).map(|ir| format!("{ir}")) +} + +#[cfg(test)] +mod test { + use crate::build::grammar::grammar; + use crate::build::ir::transform_to_ir; + use crate::fault; + use crate::fault::Fault; + use pretty_assertions::assert_str_eq; + + fn generate_ir(input: &str) -> fault::Result { + let expr = grammar::ExprParser::new() + .parse(input) + .map_err(|_| Fault::from_message("Failed to parse input"))?; + transform_to_ir(&expr) + } + + #[test] + fn test_it_generates_some_ir() { + assert_str_eq!( + "\"define i32 @main() {\\nentry:\\n ret i32 2\\n}\\n\"", + generate_ir("1+1").unwrap() + ); + } +} diff --git a/src/build/mod.rs b/src/build/mod.rs new file mode 100644 index 0000000..ea3e5f4 --- /dev/null +++ b/src/build/mod.rs @@ -0,0 +1,46 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +mod grammar; +mod ir; +mod validator; + +use crate::build::grammar::parse_file; +use crate::build::ir::transform_to_ir; +use crate::build::validator::validate; +use crate::cli::Cli; +use crate::cli::build::CommandBuild; +use crate::fault; +use crate::fault::Fault; + +pub fn build( + _cli: &Cli, + _command: &CommandBuild, + filesystem: &vfs::path::VfsPath, +) -> fault::Result<()> { + let expr = filesystem + .join("src/main.fil") + .map_err(|error| Fault::from_error(Box::from(error))) + .and_then(|main_source_file| parse_file(&main_source_file)) + .and_then(|expr| validate(&expr).map(|_| expr)) + .and_then(|expr| transform_to_ir(&expr)) + .map(|ir| println!("{ir}")); + // TODO: + // - linking into executable + + expr.and_then(|_| Err(Fault::from_message("Not yet implemented"))) +} diff --git a/src/build/validator/mod.rs b/src/build/validator/mod.rs new file mode 100644 index 0000000..3432762 --- /dev/null +++ b/src/build/validator/mod.rs @@ -0,0 +1,84 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use crate::build::grammar::ast::{Expr, Opcode}; +use crate::fault; +use crate::fault::Fault; + +pub fn validate(expr: &Expr) -> fault::Result<()> { + validate_expression(expr).map(|_| ()) +} + +fn validate_expression(expr: &Expr) -> fault::Result { + match expr { + Expr::Number(n) => Ok(*n), + Expr::Op(l, o, r) => validate_operator(l, o, r), + } +} + +fn validate_operator(left: &Expr, operator: &Opcode, right: &Expr) -> fault::Result { + match operator { + Opcode::Mul => validate_expression(right).and_then(|right_result| { + validate_expression(left).map(|left_result| left_result * right_result) + }), + Opcode::Div => validate_expression(right).and_then(|right_result| { + if right_result == 0 { + Err(Fault::from_message("You cannot divide by 0")) + } else { + validate_expression(left).map(|left_result| left_result / right_result) + } + }), + Opcode::Add => validate_expression(right).and_then(|right_result| { + validate_expression(left).map(|left_result| left_result + right_result) + }), + Opcode::Sub => validate_expression(right).and_then(|right_result| { + validate_expression(left).map(|left_result| left_result - right_result) + }), + } +} + +#[cfg(test)] +mod test { + use crate::build::grammar::grammar; + use crate::build::validator::validate_expression; + use crate::fault; + use crate::fault::Fault; + use pretty_assertions::{assert_eq, assert_str_eq}; + + fn parse_and_validate(input: &str) -> fault::Result { + grammar::ExprParser::new() + .parse(input) + .map_err(|_| Fault::from_message("Failed to parse expression")) + .and_then(|expr| validate_expression(&expr)) + } + + #[test] + fn test_validate_expression() { + assert_eq!(12, parse_and_validate("12").unwrap()); + assert_eq!(5, parse_and_validate("2 + 3").unwrap()); + assert_eq!(1, parse_and_validate("3 - 2").unwrap()); + assert_eq!(6, parse_and_validate("2 * 3").unwrap()); + assert_eq!(2, parse_and_validate("4 / 2").unwrap()); + assert_eq!(1034, parse_and_validate("22 * 44 + 66").unwrap()); + assert_eq!(2420, parse_and_validate("22 * (44 + 66)").unwrap()); + + assert_str_eq!( + "You cannot divide by 0", + format!("{}", parse_and_validate("4 / 0").err().unwrap()) + ); + } +} diff --git a/src/cli/build.rs b/src/cli/build.rs index 45d090a..cc1135f 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -15,9 +15,9 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +use crate::build::build; use crate::cli::Cli; -use crate::errors::NotImplementedError; -use crate::errors::Result; +use crate::fault; use clap::Args; #[derive(Args)] @@ -31,6 +31,10 @@ pub struct CommandBuild { pub out_dir: Option, } -pub fn run(_cli: &Cli, _command: &CommandBuild) -> Result<()> { - Err(NotImplementedError::new("build command").into()) +pub fn run( + cli: &Cli, + command: &CommandBuild, + filesystem: &vfs::path::VfsPath, +) -> fault::Result<()> { + build(cli, command, filesystem) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 9b6d67c..7e0ec61 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -15,12 +15,10 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -mod build; +pub mod build; mod new; -#[cfg(test)] -mod test; -use crate::errors::Result; +use crate::fault; use clap::builder::Styles; use clap::builder::styling::{AnsiColor, Style}; use clap::{Args, FromArgMatches, Parser, Subcommand, crate_name}; @@ -73,9 +71,55 @@ pub fn parse(args: Vec) -> Cli { .unwrap() } -pub fn run(cli: Cli) -> Result<()> { +pub fn run(cli: Cli) -> fault::Result<()> { match &cli.command { Command::New(n) => new::run(&cli, n, &vfs::PhysicalFS::new("/").into()), - Command::Build(b) => build::run(&cli, b), + Command::Build(b) => build::run(&cli, b, &vfs::PhysicalFS::new(".").into()), + } +} + +#[cfg(test)] +mod test { + use crate::cli::{Command, parse}; + use pretty_assertions::{assert_eq, assert_str_eq}; + + fn make_args(args: Vec<&str>) -> Vec { + args.iter().map(|&arg| arg.parse().unwrap()).collect() + } + + #[test] + fn it_parses_command_new_args() { + let result = parse(make_args(vec!["fil", "new", "--name", "foo"])); + match result.command { + Command::New(n) => assert_str_eq!("foo", n.name.unwrap()), + Command::Build(_) => panic!("Should have parsed command new"), + } + } + + #[test] + fn it_parses_command_new_args_default() { + let result = parse(make_args(vec!["fil", "new"])); + match result.command { + Command::New(n) => assert_eq!(None, n.name), + Command::Build(_) => panic!("Should have parsed command new"), + } + } + + #[test] + fn it_parses_command_build_args() { + let result = parse(make_args(vec!["fil", "build", "-o", "dist"])); + match result.command { + Command::New(_) => panic!("Should have parsed command build"), + Command::Build(b) => assert_str_eq!("dist", b.out_dir.unwrap()), + } + } + + #[test] + fn it_parses_command_build_args_default() { + let result = parse(make_args(vec!["fil", "build"])); + match result.command { + Command::New(_) => panic!("Should have parsed command build"), + Command::Build(b) => assert_str_eq!("build", b.out_dir.unwrap()), + } } } diff --git a/src/cli/new.rs b/src/cli/new.rs index d2e8397..e7dfc3f 100644 --- a/src/cli/new.rs +++ b/src/cli/new.rs @@ -16,9 +16,11 @@ // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. use crate::cli::Cli; -use crate::errors::{GenericError, Result}; +use crate::fault; +use crate::fault::Fault; +use crate::new::create_project; use clap::Args; -use std::process; +use cliclack::ProgressBar; #[derive(Args)] pub struct CommandNew { @@ -34,19 +36,58 @@ pub struct CommandNew { pub git: Option, } -pub fn run(_cli: &Cli, command: &CommandNew, filesystem: &vfs::path::VfsPath) -> Result<()> { - cliclack::intro(console::style(" New project ").on_green().black())?; - - cliclack::log::success("Let's create an awesome project 🤘")?; +pub fn run(_cli: &Cli, command: &CommandNew, filesystem: &vfs::path::VfsPath) -> fault::Result<()> { + cliclack::intro(console::style(" New project ").on_green().black()) + .and_then(|_| cliclack::log::success("Let's create an awesome project 🤘")) + .map_err(|error| Fault::from_error(Box::from(error))) + .map(|_| { + let spinner = cliclack::spinner(); + spinner.start("Initializing the project"); + spinner + }) + .and_then(|spinner| get_name(&command).map(|name| (spinner, name))) + .and_then(|(spinner, name)| get_git(&command).map(|git| (spinner, name, git))) + .and_then(|(spinner, name, git)| { + create_project(&name, &git, &filesystem).map(|_| (spinner, name)) + }) + .and_then(|(spinner, name): (ProgressBar, String)| { + spinner.stop("Done!"); + + cliclack::note( + "Project created! 🚀 ", + format!( + "{}\n{}{}\n", + console::style("Next steps:").bold(), + if name == "." { + String::new() + } else { + console::style(format!("cd {name}\n")).dim().to_string() + }, + "Enjoy!" + ), + ) + .map_err(|error| Fault::from_error(Box::from(error))) + }) + .and_then(|_| { + cliclack::outro(format!( + "Got problems? {}", + console::style("https://github.com/Gashmob/fil/issues/new/choose") + .yellow() + .underlined() + )) + .map_err(|error| Fault::from_error(Box::from(error))) + }) +} - let name = if let Some(given_name) = &command.name { +fn get_name(command: &CommandNew) -> fault::Result { + if let Some(given_name) = &command.name { cliclack::log::step(format!( "Project will be created with name: {}", console::style(given_name).bold() - ))?; - given_name + )) + .map(|_| given_name.clone()) } else { - &cliclack::input("How do you want to call it?") + cliclack::input("How do you want to call it?") .placeholder("blazing-fast-forward") .validate(|input: &String| { if input.is_empty() { @@ -55,137 +96,82 @@ pub fn run(_cli: &Cli, command: &CommandNew, filesystem: &vfs::path::VfsPath) -> Ok(()) } }) - .interact()? - }; + .interact() + } + .map_err(|error| Fault::from_error(Box::from(error))) +} - let git = if let Some(given_git) = &command.git { - if *given_git { +fn get_git(command: &CommandNew) -> fault::Result { + if let Some(given_git) = command.git { + if given_git { cliclack::log::step(format!( "{} will be called", console::style("git init").bold() - ))?; + )) + } else { + Ok(()) } - given_git - } else { - &cliclack::confirm("Do you want to init git?").interact()? - }; - - let spinner = cliclack::spinner(); - spinner.start("Initializing the project"); - create_project(name, git, &filesystem).and_then(|_| { - spinner.stop("Done!"); - - cliclack::note( - "Project created! 🚀 ", - format!( - "{}\n{}{}\n", - console::style("Next steps:").bold(), - if name == "." { - String::new() - } else { - console::style(format!("cd {name}\n")).dim().to_string() - }, - "Enjoy!" - ), - )?; - - cliclack::outro(format!( - "Got problems? {}", - console::style("https://github.com/Gashmob/fil/issues/new/choose") - .yellow() - .underlined() - ))?; - Ok(()) - }) -} - -fn create_project(name: &String, git: &bool, filesystem: &vfs::path::VfsPath) -> Result<()> { - let name = sanitize_name(name); - let path = if name.starts_with("/") { - filesystem.root().join(&name)? - } else { - filesystem - .join(std::env::current_dir()?.to_str().unwrap())? - .join(&name)? - }; - let name = if name.contains("/") { - path.filename() + .map(|_| given_git.clone()) } else { - name.clone() - }; - - check_path(&path) - .and_then(|_| { - path.create_dir_all() - .map_err(|err| GenericError::new(err.to_string().as_str()).into()) - }) - .and_then(|_| { - path.join("package.toml") - .map_err(|err| GenericError::new(err.to_string().as_str()).into()) - .and_then(|file| { - file.create_file() - .map_err(|err| GenericError::new(err.to_string().as_str()).into()) - }) - .and_then(|mut file_stream| { - write!(file_stream, "[package]\nname = {}", name) - .map_err(|err| GenericError::new(err.to_string().as_str()).into()) - }) - }) - .and_then(|_| { - if *git { - process::Command::new("git") - .args(vec!["init", path.as_str()]) - .output() - .map_err(|err| GenericError::new(err.to_string().as_str()).into()) - .and(Ok(())) - } else { - Ok(()) - } - }) -} - -fn check_path(path: &vfs::path::VfsPath) -> Result<()> { - path.exists() - .map_err(|err| GenericError::new(err.to_string().as_str()).into()) - .and_then(|exists| { - if exists { - path.read_dir() - .map_err(|err| GenericError::new(err.to_string().as_str()).into()) - .and_then(|read_dir| { - if read_dir.count() > 0 { - Err(GenericError::new( - format!("Directory {} is not empty", path.as_str()).as_str(), - ) - .into()) - } else { - Ok(()) - } - }) - } else { - Ok(()) - } - }) -} - -fn sanitize_name(name: &String) -> String { - let parts: Vec<_> = name.trim().split_whitespace().collect(); - parts.join("-").replace("*", "-") + cliclack::confirm("Do you want to init git?").interact() + } + .map_err(|error| Fault::from_error(Box::from(error))) } #[cfg(test)] mod test { - use crate::cli::new::sanitize_name; - use pretty_assertions::assert_eq; + use crate::cli::new::CommandNew; + use crate::cli::{Cli, Command, new}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use std::io::Read; + use vfs::{MemoryFS, VfsPath}; + + fn random_name() -> String { + format!("project_{}", rand::random::()) + } + + fn run_new(path: &VfsPath) { + let result = new::run( + &Cli { + config: "".to_string(), + command: Command::New(CommandNew { + name: Some(path.as_str().to_string()), + git: Some(false), + }), + }, + &CommandNew { + name: Some(path.as_str().to_string()), + git: Some(false), + }, + &path, + ); + assert_eq!(true, result.is_ok()); + } #[test] - fn test_sanitize_name() { - assert_eq!("foo", sanitize_name(&"foo".to_string())); - assert_eq!("foo-bar", sanitize_name(&"foo bar".to_string())); - assert_eq!("foo-bar", sanitize_name(&"foo-bar".to_string())); - assert_eq!("foo_bar", sanitize_name(&"foo_bar".to_string())); - assert_eq!("foo", sanitize_name(&" foo ".to_string())); - assert_eq!("foo-bar", sanitize_name(&" foo bar ".to_string())); - assert_eq!("foo&bar", sanitize_name(&"foo&bar".to_string())); - assert_eq!("foo-bar", sanitize_name(&"foo*bar".to_string())); + fn it_creates_project_dir() { + let root = VfsPath::new(MemoryFS::new()); + let name = random_name(); + let path = root.join(format!("/tmp/{}", name)).unwrap(); + run_new(&path); + + assert_eq!(true, path.is_dir().unwrap()); + let content: Vec<_> = path.read_dir().unwrap().collect(); + assert_eq!(vec![path.join("package.toml").unwrap()], content); + let mut package_content = String::new(); + path.join("package.toml") + .unwrap() + .open_file() + .unwrap() + .read_to_string(&mut package_content) + .unwrap(); + assert_str_eq!( + format!( + "[package] +name = {}", + name + ), + package_content + ); } } diff --git a/src/cli/test.rs b/src/cli/test.rs deleted file mode 100644 index c9a163a..0000000 --- a/src/cli/test.rs +++ /dev/null @@ -1,141 +0,0 @@ -// fil -// Copyright (C) 2026 - Present fil contributors -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -mod cli { - use crate::cli::{Command, parse}; - use pretty_assertions::assert_eq; - - fn make_args(args: Vec<&str>) -> Vec { - args.iter().map(|&arg| arg.parse().unwrap()).collect() - } - - #[test] - fn it_parses_command_new_args() { - let result = parse(make_args(vec!["fil", "new", "--name", "foo"])); - match result.command { - Command::New(n) => assert_eq!("foo", n.name.unwrap()), - Command::Build(_) => panic!("Should have parsed command new"), - } - } - - #[test] - fn it_parses_command_new_args_default() { - let result = parse(make_args(vec!["fil", "new"])); - match result.command { - Command::New(n) => assert_eq!(None, n.name), - Command::Build(_) => panic!("Should have parsed command new"), - } - } - - #[test] - fn it_parses_command_build_args() { - let result = parse(make_args(vec!["fil", "build", "-o", "dist"])); - match result.command { - Command::New(_) => panic!("Should have parsed command build"), - Command::Build(b) => assert_eq!("dist", b.out_dir.unwrap()), - } - } - - #[test] - fn it_parses_command_build_args_default() { - let result = parse(make_args(vec!["fil", "build"])); - match result.command { - Command::New(_) => panic!("Should have parsed command build"), - Command::Build(b) => assert_eq!("build", b.out_dir.unwrap()), - } - } -} - -mod new { - use crate::cli::new::CommandNew; - use crate::cli::{Cli, Command, new}; - use pretty_assertions::assert_eq; - use std::io::Read; - use vfs::{MemoryFS, VfsPath}; - - fn random_name() -> String { - format!("project_{}", rand::random::()) - } - - fn run_new(path: &VfsPath) { - let result = new::run( - &Cli { - config: "".to_string(), - command: Command::New(CommandNew { - name: Some(path.as_str().to_string()), - git: Some(false), - }), - }, - &CommandNew { - name: Some(path.as_str().to_string()), - git: Some(false), - }, - &path, - ); - assert_eq!(true, result.is_ok()); - } - - #[test] - fn it_creates_project_dir() { - let root = VfsPath::new(MemoryFS::new()); - let name = random_name(); - let path = root.join(format!("/tmp/{}", name)).unwrap(); - run_new(&path); - - assert_eq!(true, path.is_dir().unwrap()); - let content: Vec<_> = path.read_dir().unwrap().collect(); - assert_eq!(vec![path.join("package.toml").unwrap()], content); - let mut package_content = String::new(); - println!("{:?}", root); - path.join("package.toml") - .unwrap() - .open_file() - .unwrap() - .read_to_string(&mut package_content) - .unwrap(); - assert_eq!( - format!( - "[package] -name = {}", - name - ), - package_content - ); - } -} - -mod build { - use crate::cli::build::CommandBuild; - use crate::cli::{Cli, Command, build}; - use pretty_assertions::assert_eq; - - #[test] - fn it_returns_err() { - let result = build::run( - &Cli { - config: "".to_string(), - command: Command::Build(CommandBuild { out_dir: None }), - }, - &CommandBuild { out_dir: None }, - ); - assert_eq!(true, result.is_err()); - assert_eq!( - "build command is not yet implemented", - result.unwrap_err().to_string() - ); - } -} diff --git a/src/errors/mod.rs b/src/errors/mod.rs deleted file mode 100644 index 2f6e265..0000000 --- a/src/errors/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -// fil -// Copyright (C) 2026 - Present fil contributors -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -#[cfg(test)] -mod test; - -use std::error; -use std::fmt; - -pub type Result = std::result::Result>; - -#[derive(Debug, Clone)] -pub struct GenericError { - message: String, -} - -impl GenericError { - pub fn new(message: &str) -> Self { - Self { - message: message.to_string(), - } - } -} - -impl fmt::Display for GenericError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.message) - } -} - -impl error::Error for GenericError {} - -#[derive(Debug, Clone)] -pub struct NotImplementedError { - feature_name: String, -} - -impl NotImplementedError { - pub fn new(feature_name: &str) -> Self { - Self { - feature_name: feature_name.to_string(), - } - } -} - -impl fmt::Display for NotImplementedError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} is not yet implemented", self.feature_name) - } -} - -impl error::Error for NotImplementedError {} diff --git a/src/fault/mod.rs b/src/fault/mod.rs new file mode 100644 index 0000000..5438bd8 --- /dev/null +++ b/src/fault/mod.rs @@ -0,0 +1,101 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use std::fmt::Formatter; +use std::{error, fmt}; + +#[derive(Debug)] +pub struct Fault { + message: Option, + error: Option>, +} + +impl Fault { + pub fn from_message(message: &str) -> Self { + Self { + message: Some(message.to_string()), + error: None, + } + } + + pub fn from_error(error: Box) -> Self { + Self { + message: None, + error: Some(error), + } + } + + pub fn from_error_with_message(error: Box, message: &str) -> Self { + Self { + message: Some(message.to_string()), + error: Some(error), + } + } +} + +impl fmt::Display for Fault { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match (&self.message, &self.error) { + (Some(message), None) => write!(f, "{message}"), + (Some(message), Some(error)) => write!(f, "{message}: {error}"), + (None, Some(error)) => write!(f, "{error}"), + _ => write!(f, "Got an unknown fault, please open an issue"), + } + } +} + +pub type Result = std::result::Result; + +#[cfg(test)] +mod test { + use crate::fault::Fault; + use pretty_assertions::assert_str_eq; + use std::fmt::Formatter; + use std::{error, fmt}; + + #[derive(Debug)] + struct ErrorStub {} + + impl fmt::Display for ErrorStub { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Some stub error") + } + } + + impl error::Error for ErrorStub {} + + #[test] + fn test_fault_from_message() { + assert_str_eq!("Oh snap!", Fault::from_message("Oh snap!").to_string()) + } + + #[test] + fn test_fault_from_error() { + assert_str_eq!( + "Some stub error", + Fault::from_error(Box::new(ErrorStub {})).to_string() + ) + } + + #[test] + fn test_fault_from_error_with_message() { + assert_str_eq!( + "Oopsie: Some stub error", + Fault::from_error_with_message(Box::new(ErrorStub {}), "Oopsie").to_string() + ) + } +} diff --git a/src/main.rs b/src/main.rs index 6b598ee..ec0d348 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,10 +16,19 @@ // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. use std::env; +use std::process::ExitCode; +mod build; mod cli; -mod errors; +mod fault; +mod new; -fn main() -> Result<(), String> { - cli::run(cli::parse(env::args().collect())).map_err(|err| err.to_string()) +fn main() -> ExitCode { + match cli::run(cli::parse(env::args().collect())) { + Ok(_) => ExitCode::SUCCESS, + Err(err) => { + eprintln!("{err}"); + ExitCode::FAILURE + } + } } diff --git a/src/new/mod.rs b/src/new/mod.rs new file mode 100644 index 0000000..11fda75 --- /dev/null +++ b/src/new/mod.rs @@ -0,0 +1,160 @@ +// fil +// Copyright (C) 2026 - Present fil contributors +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use crate::fault; +use crate::fault::Fault; +use std::process; +use vfs::VfsPath; + +pub fn create_project(name: &String, git: &bool, filesystem: &VfsPath) -> fault::Result<()> { + let name = sanitize_name(name); + get_path(&filesystem, &name) + .map(|path| (path.clone(), get_name(&name, &path))) + .and_then(|(path, name)| check_path_is_empty(&path).map(|_| (path, name))) + .and_then(|(path, name)| { + path.create_dir_all() + .map_err(|error| Fault::from_error(Box::from(error))) + .map(|_| (path, name)) + }) + .and_then(|(path, name)| init_project(&path, &name).map(|_| path)) + .and_then(|path| init_git(&git, &path)) +} + +fn get_path(filesystem: &VfsPath, name: &String) -> fault::Result { + if name.starts_with("/") { + filesystem + .root() + .join(&name) + .map_err(|error| Fault::from_error(Box::from(error))) + } else { + std::env::current_dir() + .map_err(|error| Fault::from_error(Box::from(error))) + .and_then(|current_dir_path| { + if let Some(current_dir) = current_dir_path.to_str() { + Ok(String::from(current_dir)) + } else { + Err(Fault::from_message("")) + } + }) + .and_then(|current_dir| { + filesystem + .join(current_dir) + .map_err(|error| Fault::from_error(Box::from(error))) + }) + .and_then(|_| { + filesystem + .join(&name) + .map_err(|error| Fault::from_error(Box::from(error))) + }) + } +} + +fn get_name(name: &String, path: &VfsPath) -> String { + if name.contains("/") { + path.filename() + } else { + name.clone() + } +} + +fn check_path_is_empty(path: &VfsPath) -> fault::Result<()> { + path.exists() + .map_err(|error| Fault::from_error(Box::from(error))) + .and_then(|exists| { + if exists { + path.read_dir() + .map_err(|error| Fault::from_error(Box::from(error))) + .and_then(|read_dir| { + if read_dir.count() > 0 { + Err(Fault::from_message( + format!("Directory {} is not empty", path.as_str()).as_str(), + )) + } else { + Ok(()) + } + }) + } else { + Ok(()) + } + }) +} + +fn init_project(path: &VfsPath, name: &String) -> fault::Result<()> { + path.join("package.toml") + .map_err(|error| Fault::from_error(Box::from(error))) + .and_then(|file| { + file.create_file() + .map_err(|error| Fault::from_error(Box::from(error))) + }) + .and_then(|mut file_stream| { + write!(file_stream, "[package]\nname = {}", name) + .map_err(|error| Fault::from_error(Box::from(error))) + }) +} + +fn init_git(git: &bool, path: &VfsPath) -> fault::Result<()> { + if *git { + process::Command::new("git") + .args(vec!["init", path.as_str()]) + .output() + .map_err(|error| Fault::from_error(Box::from(error))) + .and(Ok(())) + } else { + Ok(()) + } +} + +fn sanitize_name(name: &String) -> String { + let parts: Vec<_> = name.trim().split_whitespace().collect(); + parts.join("-").replace("*", "-") +} + +#[cfg(test)] +mod test { + use crate::new::{check_path_is_empty, sanitize_name}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use vfs::{MemoryFS, VfsPath}; + + #[test] + fn test_check_path() { + let root = VfsPath::new(MemoryFS::new()); + root.join("foo").unwrap().create_dir().unwrap(); + root.join("bar").unwrap().create_dir().unwrap(); + root.join("bar/lorem").unwrap().create_file().unwrap(); + + assert_eq!( + true, + check_path_is_empty(&root.join("foo").unwrap()).is_ok() + ); + assert_eq!( + true, + check_path_is_empty(&root.join("bar").unwrap()).is_err() + ); + } + + #[test] + fn test_sanitize_name() { + assert_str_eq!("foo", sanitize_name(&"foo".to_string())); + assert_str_eq!("foo-bar", sanitize_name(&"foo bar".to_string())); + assert_str_eq!("foo-bar", sanitize_name(&"foo-bar".to_string())); + assert_str_eq!("foo_bar", sanitize_name(&"foo_bar".to_string())); + assert_str_eq!("foo", sanitize_name(&" foo ".to_string())); + assert_str_eq!("foo-bar", sanitize_name(&" foo bar ".to_string())); + assert_str_eq!("foo&bar", sanitize_name(&"foo&bar".to_string())); + assert_str_eq!("foo-bar", sanitize_name(&"foo*bar".to_string())); + } +}