diff options
-rw-r--r-- | Cargo.lock | 641 | ||||
-rw-r--r-- | crates/rebel-lang/Cargo.toml | 24 | ||||
-rw-r--r-- | crates/rebel-lang/benches/recipe.rs | 104 | ||||
-rw-r--r-- | crates/rebel-lang/examples/repl.rs | 141 | ||||
-rw-r--r-- | crates/rebel-lang/src/func.rs | 33 | ||||
-rw-r--r-- | crates/rebel-lang/src/lib.rs | 36 | ||||
-rw-r--r-- | crates/rebel-lang/src/scope.rs | 277 | ||||
-rw-r--r-- | crates/rebel-lang/src/typing.rs | 791 | ||||
-rw-r--r-- | crates/rebel-lang/src/value.rs | 713 | ||||
-rw-r--r-- | crates/rebel-parse/Cargo.toml | 5 | ||||
-rw-r--r-- | crates/rebel-parse/examples/parse-string.rs | 12 | ||||
-rw-r--r-- | crates/rebel-parse/src/ast.rs | 223 | ||||
-rw-r--r-- | crates/rebel-parse/src/ast/expr.rs | 333 | ||||
-rw-r--r-- | crates/rebel-parse/src/ast/mod.rs | 187 | ||||
-rw-r--r-- | crates/rebel-parse/src/ast/pat.rs | 57 | ||||
-rw-r--r-- | crates/rebel-parse/src/ast/typ.rs | 28 | ||||
-rw-r--r-- | crates/rebel-parse/src/grammar/recipe.rs | 245 | ||||
-rw-r--r-- | crates/rebel-parse/src/grammar/tokenize.rs | 77 | ||||
-rw-r--r-- | crates/rebel-parse/src/token.rs | 37 | ||||
-rw-r--r-- | examples/recipes/gmp/build.recipe | 42 |
20 files changed, 3605 insertions, 401 deletions
@@ -3,48 +3,64 @@ version = 3 [[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -63,6 +79,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] name = "bincode" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -82,6 +104,9 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] [[package]] name = "blake3" @@ -107,6 +132,12 @@ dependencies = [ ] [[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] name = "capctl" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -119,9 +150,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -136,6 +167,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.5", +] + +[[package]] name = "clap" version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -164,10 +208,10 @@ version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -178,9 +222,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "condtype" @@ -195,6 +239,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -204,6 +254,32 @@ dependencies = [ ] [[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "serde", + "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" @@ -220,6 +296,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b390250ca2b6862735ef39a7b37b0266a8d5cd9d1e579260c95b1dc27761d6ad" [[package]] +name = "derive-into-owned" +version = "0.2.0" +source = "git+https://github.com/neocturne/derive-into-owned.git?branch=more-types#e692ae6da9220ff3a45b0a1c380e1f4a90c5b890" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -252,10 +338,16 @@ checksum = "27540baf49be0d484d8f0130d7d8da3011c32a44d4fc873368154f1510e574a2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] name = "enum-kinds" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -283,6 +375,17 @@ dependencies = [ ] [[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] name = "filetime" version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -290,7 +393,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -320,9 +423,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" @@ -340,6 +449,29 @@ dependencies = [ ] [[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -356,12 +488,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -369,9 +525,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "linux-raw-sys" @@ -380,6 +536,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[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.21" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -392,6 +558,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[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 = "nix" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -404,6 +582,24 @@ dependencies = [ ] [[package]] +name = "nu-ansi-term" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "olpc-cjson" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -421,9 +617,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] +name = "parking_lot" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +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 0.5.1", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] name = "peg" -version = "0.8.2" -source = "git+https://github.com/kevinmehall/rust-peg.git#2fc1cadaa1efcf47c867715d063ac2d7296945c6" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a625d12ad770914cbf7eff6f9314c3ef803bfe364a1b20bc36ddf56673e71e5" dependencies = [ "peg-macros", "peg-runtime", @@ -431,8 +651,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.8.2" -source = "git+https://github.com/kevinmehall/rust-peg.git#2fc1cadaa1efcf47c867715d063ac2d7296945c6" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f241d42067ed3ab6a4fece1db720838e1418f36d868585a27931f95d6bc03582" dependencies = [ "peg-runtime", "proc-macro2", @@ -441,14 +662,15 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.8.2" -source = "git+https://github.com/kevinmehall/rust-peg.git#2fc1cadaa1efcf47c867715d063ac2d7296945c6" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" [[package]] name = "pest" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -457,9 +679,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -467,22 +689,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] name = "pest_meta" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -490,10 +712,52 @@ dependencies = [ ] [[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -508,6 +772,21 @@ dependencies = [ ] [[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] name = "rebel" version = "0.1.0" dependencies = [ @@ -534,13 +813,29 @@ dependencies = [ ] [[package]] +name = "rebel-lang" +version = "0.1.0" +dependencies = [ + "clap", + "divan", + "enum-kinds", + "rebel-common", + "rebel-parse", + "reedline", + "rustc-hash", +] + +[[package]] name = "rebel-parse" version = "0.1.0" dependencies = [ "clap", + "derive-into-owned", "divan", "peg", + "phf", "rebel-common", + "rustc-hash", ] [[package]] @@ -583,12 +878,47 @@ dependencies = [ ] [[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "reedline" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf59e4c97b5049ba96b052cdb652368305a2eddcbce9bf1c16f9d003139eeea" +dependencies = [ + "chrono", + "crossterm", + "fd-lock", + "itertools", + "nu-ansi-term", + "serde", + "strip-ansi-escapes", + "strum", + "strum_macros", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + +[[package]] name = "regex-lite" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" [[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -602,10 +932,16 @@ dependencies = [ ] [[package]] +name = "rustversion" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" + +[[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -617,23 +953,29 @@ dependencies = [ ] [[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.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -672,12 +1014,82 @@ dependencies = [ ] [[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +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 = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + +[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.61", +] + +[[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -696,9 +1108,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -734,22 +1146,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -804,6 +1216,18 @@ dependencies = [ ] [[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + +[[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -822,6 +1246,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -832,6 +1276,82 @@ dependencies = [ ] [[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.61", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[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.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -841,6 +1361,21 @@ dependencies = [ ] [[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/crates/rebel-lang/Cargo.toml b/crates/rebel-lang/Cargo.toml new file mode 100644 index 0000000..a0a04a6 --- /dev/null +++ b/crates/rebel-lang/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rebel-lang" +version = "0.1.0" +authors = ["Matthias Schiffer <mschiffer@universe-factory.net>"] +license = "MIT" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rebel-common = { path = "../rebel-common" } +rebel-parse = { path = "../rebel-parse" } + +enum-kinds = "0.5.1" +rustc-hash = "1.1.0" + +[dev-dependencies] +clap = { version = "4.0.0", features = ["derive"] } +divan = "0.1.14" +reedline = "0.32.0" + +[[bench]] +name = "recipe" +harness = false diff --git a/crates/rebel-lang/benches/recipe.rs b/crates/rebel-lang/benches/recipe.rs new file mode 100644 index 0000000..fe017f5 --- /dev/null +++ b/crates/rebel-lang/benches/recipe.rs @@ -0,0 +1,104 @@ +use rebel_lang::{ + scope::Scope, + typing::{self, Type, VarType}, + value::{self, Value}, +}; +use rebel_parse::ast; + +fn main() { + divan::main(); +} + +const RECIPE: &str = include_str!("../../../examples/recipes/gmp/build.recipe"); + +fn recipe() -> ast::Recipe<'static> { + let tokens = rebel_parse::tokenize::token_stream(RECIPE).unwrap(); + rebel_parse::recipe::recipe(&tokens).unwrap() +} + +fn type_scope() -> Box<Scope<VarType>> { + let mut scope = Box::<Scope<_>>::default(); + + scope.defs.insert_value("workdir", VarType::new(Type::Str)); + scope.initialize("workdir"); + scope.defs.insert_value("name", VarType::new(Type::Str)); + scope.initialize("name"); + + scope +} + +fn value_scope() -> Box<Scope<Value>> { + let mut scope = Box::<Scope<_>>::default(); + + scope + .defs + .insert_value("workdir", Value::Str("workdir".to_owned())); + scope + .defs + .insert_value("name", Value::Str("gpm".to_owned())); + + scope +} + +#[divan::bench] +fn validate(bencher: divan::Bencher) { + let recipe = recipe(); + + bencher.bench(|| { + for stmt in divan::black_box(&recipe) { + stmt.validate().unwrap(); + } + }); +} + +#[divan::bench] +fn typecheck(bencher: divan::Bencher) { + let recipe = recipe(); + let scope = type_scope(); + + for stmt in &recipe { + stmt.validate().unwrap(); + } + + bencher + .with_inputs(|| scope.clone()) + .bench_local_values(|mut scope| { + let mut ctx = typing::Context(&mut scope); + + for stmt in divan::black_box(&recipe) { + let ast::RecipeStmt::BlockStmt(stmt) = stmt else { + // TODO: Check other statements + continue; + }; + + ctx.type_block_stmt(stmt).unwrap(); + } + scope + }); +} + +#[divan::bench] +fn execute(bencher: divan::Bencher) { + let recipe = recipe(); + let scope = value_scope(); + + for stmt in &recipe { + stmt.validate().unwrap(); + } + + bencher + .with_inputs(|| scope.clone()) + .bench_local_values(|mut scope| { + let mut ctx = value::Context(&mut scope); + + for stmt in divan::black_box(&recipe) { + let ast::RecipeStmt::BlockStmt(stmt) = stmt else { + // TODO: Execute other statements + continue; + }; + + ctx.eval_block_stmt(stmt).unwrap(); + } + scope + }); +} diff --git a/crates/rebel-lang/examples/repl.rs b/crates/rebel-lang/examples/repl.rs new file mode 100644 index 0000000..3e0c319 --- /dev/null +++ b/crates/rebel-lang/examples/repl.rs @@ -0,0 +1,141 @@ +use std::rc::Rc; + +use rebel_lang::{ + func::{Func, FuncDef, FuncType}, + scope::{MethodMap, Scope}, + typing::{self, Type, TypeFamily, VarType}, + value::{self, Value}, + Error, Result, +}; +use rebel_parse::{recipe, tokenize}; +use reedline::{DefaultPrompt, DefaultPromptSegment, Reedline, Signal, ValidationResult}; + +fn intrinsic_array_len(params: &[Value]) -> Result<Value> { + assert!(params.len() == 1); + let Value::Array(array) = ¶ms[0] else { + panic!(); + }; + Ok(Value::Int( + array + .len() + .try_into() + .or(Err(Error::eval("array length out of bounds")))?, + )) +} +fn intrinsic_string_len(params: &[Value]) -> Result<Value> { + assert!(params.len() == 1); + let Value::Str(string) = ¶ms[0] else { + panic!(); + }; + Ok(Value::Int( + string + .chars() + .count() + .try_into() + .or(Err(Error::eval("string length out of bounds")))?, + )) +} + +struct Validator; + +impl reedline::Validator for Validator { + fn validate(&self, line: &str) -> ValidationResult { + if tokenize::token_stream(line).is_ok() { + ValidationResult::Complete + } else { + ValidationResult::Incomplete + } + } +} + +fn main() { + let mut methods = MethodMap::default(); + methods.entry(TypeFamily::Array).or_default().insert( + "len", + Func { + typ: FuncType { + params: vec![Type::Array(Box::new(Type::Free))], + ret: Type::Int, + }, + def: FuncDef::Intrinsic(intrinsic_array_len), + }, + ); + methods.entry(TypeFamily::Str).or_default().insert( + "len", + Func { + typ: FuncType { + params: vec![Type::Str], + ret: Type::Int, + }, + def: FuncDef::Intrinsic(intrinsic_string_len), + }, + ); + let methods = Rc::new(methods); + + let mut type_scope = Box::<Scope<VarType>>::default(); + type_scope.methods = methods.clone(); + let mut value_scope = Box::<Scope<Value>>::default(); + value_scope.methods = methods.clone(); + + let mut rl = Reedline::create().with_validator(Box::new(Validator)); + let prompt = DefaultPrompt::new(DefaultPromptSegment::Empty, DefaultPromptSegment::Empty); + + 'repl: loop { + let input = match rl.read_line(&prompt).unwrap() { + Signal::Success(input) => input, + Signal::CtrlC => continue, + Signal::CtrlD => break, + }; + + let tokens = match tokenize::token_stream(&input) { + Ok(value) => value, + Err(err) => { + println!("Tokenize error: {err}"); + continue; + } + }; + let block = match recipe::block(&tokens) { + Ok(value) => value, + Err(err) => { + println!("Parse error: {err}"); + continue; + } + }; + + let mut type_scope_tmp = type_scope.clone(); + let mut value_scope_tmp = value_scope.clone(); + + let mut typ = Type::unit(); + let mut value = Value::unit(); + + for stmt in &block.0 { + if let Err(err) = stmt.validate() { + println!("Validation error: {err:?}"); + continue 'repl; + } + + typ = match typing::Context(&mut type_scope_tmp).type_block_stmt(stmt) { + Ok(typ) => typ, + Err(err) => { + println!("Type checking failed: {err}"); + continue 'repl; + } + }; + + value = match value::Context(&mut value_scope_tmp).eval_block_stmt(stmt) { + Ok(value) => value, + Err(err) => { + println!("Evaluation failed: {err}"); + continue 'repl; + } + }; + } + + if value != Value::unit() { + println!("{value}: {typ}"); + } + + type_scope = type_scope_tmp; + value_scope = value_scope_tmp; + } +} diff --git a/crates/rebel-lang/src/func.rs b/crates/rebel-lang/src/func.rs new file mode 100644 index 0000000..19d3ea0 --- /dev/null +++ b/crates/rebel-lang/src/func.rs @@ -0,0 +1,33 @@ +use std::fmt::Display; + +use rebel_parse::ast::Block; + +use crate::{typing::Type, value::Value, Result}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Func { + pub typ: FuncType, + pub def: FuncDef, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FuncType { + pub params: Vec<Type>, + pub ret: Type, +} + +impl Display for FuncType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // TODO + write!(f, "fn( ... ) -> {}", self.ret) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FuncDef { + Intrinsic(fn(&[Value]) -> Result<Value>), + Body { + param_names: Vec<String>, + block: Block<'static>, + }, +} diff --git a/crates/rebel-lang/src/lib.rs b/crates/rebel-lang/src/lib.rs new file mode 100644 index 0000000..001bb79 --- /dev/null +++ b/crates/rebel-lang/src/lib.rs @@ -0,0 +1,36 @@ +use std::fmt::Display; + +pub mod func; +pub mod scope; +pub mod typing; +pub mod value; + +#[derive(Debug)] +pub struct Error(pub &'static str, pub &'static str); + +impl Error { + pub fn lookup(text: &'static str) -> Self { + Self("Lookup", text) + } + + pub fn typ(text: &'static str) -> Self { + Self("Type", text) + } + + pub fn eval(text: &'static str) -> Self { + Self("Eval", text) + } + + pub fn bug(text: &'static str) -> Self { + Self("BUG", text) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Error(cat, text) = self; + write!(f, "{cat} error: {text}") + } +} + +pub type Result<T> = std::result::Result<T, Error>; diff --git a/crates/rebel-lang/src/scope.rs b/crates/rebel-lang/src/scope.rs new file mode 100644 index 0000000..decb427 --- /dev/null +++ b/crates/rebel-lang/src/scope.rs @@ -0,0 +1,277 @@ +use std::{borrow::Cow, mem, rc::Rc}; + +use rebel_parse::ast::{self, pat}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + func::Func, + typing::{Type, TypeFamily}, + Error, Result, +}; + +pub type MethodMap = FxHashMap<TypeFamily, FxHashMap<&'static str, Func>>; + +#[derive(Debug, Clone)] +pub struct Scope<T> { + pub defs: Module<T>, + pub initialized: Rc<FxHashSet<String>>, + pub methods: Rc<MethodMap>, + pub parent: Option<Box<Scope<T>>>, + pub upvalue_initialized: FxHashSet<String>, +} + +impl<T> Default for Scope<T> { + fn default() -> Self { + let mut ret = Scope { + defs: Module::default(), + initialized: Rc::default(), + methods: Rc::default(), + parent: None, + upvalue_initialized: FxHashSet::default(), + }; + + // Type "prelude" + ret.defs.insert_type("bool", Type::Bool); + ret.defs.insert_type("int", Type::Int); + ret.defs.insert_type("str", Type::Str); + + ret + } +} + +impl<T> Scope<T> { + pub fn is_initialized(&self, path: &ast::Path) -> bool { + // TODO: What to do about other paths? + if let [ident] = &path.components[..] { + self.initialized.contains(ident.name.as_ref()) + } else { + true + } + } + + pub fn initialize(&mut self, ident: &str) { + if !self.initialized.contains(ident) { + Rc::make_mut(&mut self.initialized).insert(ident.to_owned()); + } + if !self.defs.values.contains_key(ident) { + self.upvalue_initialized.insert(ident.to_owned()); + } + } + + pub fn initialize_all(&mut self, idents: impl IntoIterator<Item = impl AsRef<str>>) { + for ident in idents { + self.initialize(ident.as_ref()); + } + } + + pub fn func(&self) -> Self { + // TODO: Upvalues + Scope { + defs: Module::default(), + initialized: Rc::default(), + methods: self.methods.clone(), + parent: None, + upvalue_initialized: FxHashSet::default(), + } + } + + pub fn scoped<F, R>(self: &mut Box<Self>, f: F) -> (R, FxHashSet<String>) + where + F: FnOnce(&mut Box<Self>) -> R, + { + let child = Box::new(Scope { + defs: Module::default(), + initialized: self.initialized.clone(), + methods: self.methods.clone(), + parent: None, + upvalue_initialized: FxHashSet::default(), + }); + let scope = mem::replace(self, child); + self.parent = Some(scope); + + let ret = f(self); + + let parent = self.parent.take().unwrap(); + let child = mem::replace(self, parent); + let Scope { + upvalue_initialized, + .. + } = *child; + + (ret, upvalue_initialized) + } + + pub fn lookup_value(&self, path: &ast::Path) -> Result<&T> { + if path.root != ast::PathRoot::Relative { + return Err(Error::lookup("invalid path")); + } + + let mut lookup_scope = Some(self); + while let Some(scope) = lookup_scope { + if scope.defs.contains_value(&path.components) { + return scope + .defs + .lookup_value(&path.components) + .ok_or(Error::lookup("unresolved path")); + } + lookup_scope = scope.parent.as_deref(); + } + + Err(Error::lookup("undefined variable")) + } + + pub fn lookup_type(&self, path: &ast::Path) -> Result<&Type> { + if path.root != ast::PathRoot::Relative { + return Err(Error::lookup("invalid path")); + } + if path.components + == [ast::Ident { + name: Cow::Borrowed("_"), + }] { + return Ok(&Type::Free); + } + + let mut lookup_scope = Some(self); + while let Some(scope) = lookup_scope { + if scope.defs.contains_type(&path.components) { + return scope + .defs + .lookup(&path.components) + .and_then(|def| def.typ.as_deref()) + .ok_or(Error::lookup("unresolved path")); + } + lookup_scope = scope.parent.as_deref(); + } + + Err(Error::lookup("undefined type")) + } +} + +impl<T: Clone> Scope<T> { + pub fn lookup_var_mut<'a>(&mut self, path: &ast::Path<'a>) -> Result<(&mut T, ast::Ident<'a>)> { + if path.root != ast::PathRoot::Relative { + return Err(Error::lookup("invalid path")); + } + + let mut lookup_scope = Some(self); + while let Some(scope) = lookup_scope { + if scope.defs.contains_value(&path.components) { + return scope + .defs + .lookup_value_mut(&path.components) + .ok_or(Error::lookup("unresolved path")); + } + lookup_scope = scope.parent.as_deref_mut(); + } + + Err(Error::lookup("undefined variable")) + } +} + +pub fn pat_ident<'a>(pat: &pat::Pat<'a>) -> ast::Ident<'a> { + match pat { + pat::Pat::Paren(subpat) => pat_ident(subpat), + pat::Pat::Ident(ident) => ident.clone(), + } +} + +pub fn is_wildcard_destr_pat(pat: &pat::DestrPat) -> bool { + match pat { + pat::DestrPat::Index { .. } => false, + pat::DestrPat::Field { .. } => false, + pat::DestrPat::Paren(pat) => is_wildcard_destr_pat(pat), + pat::DestrPat::Path(path) => { + path.root == ast::PathRoot::Relative + && path.components + == [ast::Ident { + name: Cow::Borrowed("_"), + }] + } + } +} + +#[derive(Debug)] +pub struct Module<T> { + pub values: FxHashMap<String, Rc<T>>, + pub typ: Option<Rc<Type>>, + pub children: Rc<FxHashMap<String, Module<T>>>, +} + +impl<T> Module<T> { + pub fn contains_value(&self, path: &[ast::Ident<'_>]) -> bool { + match path { + [] => true, + [ident] => self.values.contains_key(ident.name.as_ref()), + [ident, ..] => self.children.contains_key(ident.name.as_ref()), + } + } + + pub fn contains_type(&self, path: &[ast::Ident<'_>]) -> bool { + match path { + [] => true, + [ident, ..] => self.children.contains_key(ident.name.as_ref()), + } + } + + pub fn insert_value(&mut self, ident: &str, value: T) { + self.values.insert(ident.to_owned(), Rc::new(value)); + } + + pub fn insert_type(&mut self, ident: &str, typ: Type) { + Rc::make_mut(&mut self.children) + .entry(ident.to_owned()) + .or_default() + .typ = Some(Rc::new(typ)); + } + + pub fn lookup(&self, path: &[ast::Ident<'_>]) -> Option<&Module<T>> { + let Some((ident, rest)) = path.split_first() else { + return Some(self); + }; + + self.children.get(ident.name.as_ref())?.lookup(rest) + } + + pub fn lookup_value(&self, path: &[ast::Ident<'_>]) -> Option<&T> { + let (ident, prefix) = path.split_last()?; + let module = self.lookup(prefix)?; + module.values.get(ident.name.as_ref()).map(Rc::as_ref) + } +} + +impl<T: Clone> Module<T> { + pub fn lookup_value_mut<'a>( + &mut self, + path: &[ast::Ident<'a>], + ) -> Option<(&mut T, ast::Ident<'a>)> { + if let [ident] = path { + return Some(( + self.values.get_mut(ident.name.as_ref()).map(Rc::make_mut)?, + ident.clone(), + )); + } + + // TODO: Decide how to handle this case + None + } +} + +impl<T> Default for Module<T> { + fn default() -> Self { + Self { + values: Default::default(), + typ: Default::default(), + children: Default::default(), + } + } +} + +impl<T> Clone for Module<T> { + fn clone(&self) -> Self { + Self { + values: self.values.clone(), + typ: self.typ.clone(), + children: self.children.clone(), + } + } +} diff --git a/crates/rebel-lang/src/typing.rs b/crates/rebel-lang/src/typing.rs new file mode 100644 index 0000000..34280cd --- /dev/null +++ b/crates/rebel-lang/src/typing.rs @@ -0,0 +1,791 @@ +use std::{fmt::Display, iter, rc::Rc}; + +use enum_kinds::EnumKind; + +use rebel_parse::ast::{self, expr, pat, typ}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + func::FuncType, + scope::{self, Scope}, + Error, Result, +}; + +#[derive(Debug)] +pub struct Context<'scope>(pub &'scope mut Box<Scope<VarType>>); + +#[derive(Debug)] +pub struct TypeContext<'scope, T>(pub &'scope Scope<T>); + +#[derive(Debug, Clone)] +pub struct VarType { + pub explicit_type: Type, + pub inferred_type: Type, +} + +impl VarType { + pub fn new(explicit_type: Type) -> Self { + VarType { + inferred_type: explicit_type.clone(), + explicit_type, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Coerce { + Common, + Compare, + Assign, +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumKind)] +#[enum_kind(TypeFamily, derive(Hash))] +pub enum Type { + Free, + Bool, + Int, + Str, + Option(Box<Type>), + Tuple(Vec<Type>), + Array(Box<Type>), + Map(OrdType, Box<Type>), + Struct(FxHashMap<String, Type>), + Fn(Box<FuncType>), +} + +impl Type { + pub fn unit() -> Self { + Type::Tuple(Vec::new()) + } + + pub fn unify(self, other: Type, coerce: Coerce) -> Result<Type> { + use Type::*; + + Ok(match (self, other) { + (Free, typ) => typ, + (typ, Free) => typ, + (Bool, Bool) => Bool, + (Int, Int) => Int, + (Str, Str) => Str, + (Option(self_inner), Option(other_inner)) => { + Option(Box::new(self_inner.unify(*other_inner, coerce)?)) + } + (Option(self_inner), other_typ) => { + Option(Box::new(self_inner.unify(other_typ, coerce)?)) + } + (self_typ, Option(other_inner)) if coerce != Coerce::Assign => { + Option(Box::new(self_typ.unify(*other_inner, coerce)?)) + } + (Tuple(self_elems), Tuple(other_elems)) if self_elems.len() == other_elems.len() => { + Tuple( + self_elems + .into_iter() + .zip(other_elems.into_iter()) + .map(|(t1, t2)| t1.unify(t2, coerce)) + .collect::<Result<_>>()?, + ) + } + (Array(self_inner), Array(other_inner)) => { + Array(Box::new(self_inner.unify(*other_inner, coerce)?)) + } + (Map(self_key, self_value), Map(other_key, other_value)) if self_key == other_key => { + Map(self_key, Box::new(self_value.unify(*other_value, coerce)?)) + } + (Struct(self_entries), Struct(mut other_entries)) => { + if self_entries.len() != other_entries.len() { + return Err(Error::typ("conflicting struct types")); + } + Struct( + self_entries + .into_iter() + .map(|(k, v)| { + let Some(v2) = other_entries.remove(&k) else { + return Err(Error::typ("conflicting struct types")); + }; + Ok((k, v.unify(v2, coerce)?)) + }) + .collect::<Result<_>>()?, + ) + } + _ => return Err(Error::typ("type conflict")), + }) + } +} + +impl From<OrdType> for Type { + fn from(value: OrdType) -> Self { + match value { + OrdType::Free => Type::Free, + OrdType::Int => Type::Int, + OrdType::Str => Type::Str, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OrdType { + Free, + Int, + Str, +} + +impl OrdType { + pub fn unify(self, other: OrdType, _coerce: Coerce) -> Result<OrdType> { + use OrdType::*; + + Ok(match (self, other) { + (Free, typ) => typ, + (typ, Free) => typ, + (Int, Int) => Int, + (Str, Str) => Str, + _ => return Err(Error::typ("type conflict")), + }) + } +} + +impl Display for OrdType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OrdType::Free => f.write_str("_"), + OrdType::Int => f.write_str("int"), + OrdType::Str => f.write_str("str"), + } + } +} + +impl TryFrom<Type> for OrdType { + type Error = Error; + + fn try_from(value: Type) -> Result<Self> { + Ok(match value { + Type::Free => OrdType::Free, + Type::Int => OrdType::Int, + Type::Str => OrdType::Str, + _ => return Err(Error::typ("invalid type for set/map key")), + }) + } +} + +impl<'scope> Context<'scope> { + pub fn type_block_stmt(&mut self, stmt: &ast::BlockStmt) -> Result<Type> { + Ok(match stmt { + ast::BlockStmt::Let { dest, expr } => { + let ast::TypedPat { pat, typ } = dest.as_ref(); + + let dest_ident = scope::pat_ident(pat); + + let explicit_type = if let Some(typ) = typ { + let type_ctx = TypeContext(self.0); + type_ctx.type_type(typ)? + } else { + Type::Free + }; + + let expr_type = expr + .as_ref() + .map(|expr| { + let expr_type = self.type_expr(expr)?; + explicit_type + .clone() + .unify(expr_type.clone(), Coerce::Assign)?; + Ok(expr_type) + }) + .transpose()?; + + if dest_ident.name == "_" { + return Ok(Type::unit()); + } + + self.0 + .defs + .insert_value(dest_ident.name.as_ref(), VarType::new(explicit_type)); + Rc::make_mut(&mut self.0.initialized).remove(dest_ident.name.as_ref()); + + let Some(expr_type) = expr_type else { + return Ok(Type::unit()); + }; + + self.assign_destr_pat_type(&pat::DestrPat::from(pat.borrowed()), expr_type)? + } + ast::BlockStmt::Assign { dest, expr } => { + let expr_type = self.type_expr(expr)?; + self.assign_destr_pat_type(dest, expr_type)? + } + ast::BlockStmt::Fn { + ident, + params, + ret, + block, + } => { + let typ = Type::Fn(Box::new(self.type_fn_def(params, ret.as_deref(), block)?)); + + // TODO: Reject in validation? + if ident.name == "_" { + return Ok(Type::unit()); + } + self.0 + .defs + .insert_value(ident.name.as_ref(), VarType::new(typ.clone())); + Rc::make_mut(&mut self.0.initialized).insert(ident.name.as_ref().to_owned()); + + typ + } + ast::BlockStmt::Expr { expr } => self.type_expr(expr)?, + ast::BlockStmt::Empty => Type::unit(), + }) + } + + fn type_fn_def( + &mut self, + params: &[ast::FuncParam], + ret: Option<&typ::Type>, + block: &ast::Block, + ) -> Result<FuncType> { + let type_ctx = TypeContext(self.0); + let typ = type_ctx.type_fn_def(params, ret)?; + + let mut scope = Box::new(self.0.func()); + + { + let defs = &mut scope.defs; + let initialized = Rc::make_mut(&mut scope.initialized); + + for (param, param_type) in params.iter().zip(&typ.params) { + defs.insert_value(param.name.name.as_ref(), VarType::new(param_type.clone())); + initialized.insert(param.name.name.as_ref().to_owned()); + } + } + + typ.ret + .clone() + .unify(Context(&mut scope).type_block(block)?, Coerce::Assign)?; + + Ok(typ) + } + + pub fn type_expr(&mut self, expr: &expr::Expr<'_>) -> Result<Type> { + use expr::Expr::*; + + match expr { + Binary { left, op, right } => self.type_binary_op(left, *op, right), + Unary { op, expr } => self.type_unary_op(*op, expr), + Apply { expr, params } => self.type_apply(expr, params), + Method { + expr, + method, + params, + } => self.type_method(expr, method, params), + Index { base, index } => self.type_index(base, index), + Field { base, field } => self.type_field(base, field), + Block(block) => self.type_block(block), + IfElse { + if_blocks, + else_block, + } => self.type_ifelse(if_blocks, else_block), + Paren(subexpr) => self.type_expr(subexpr), + Path(path) => self.type_path(path), + Literal(lit) => self.type_literal(lit), + } + } + + fn type_binary_op( + &mut self, + left: &expr::Expr<'_>, + op: expr::OpBinary, + right: &expr::Expr<'_>, + ) -> Result<Type> { + use expr::OpBinary::*; + use Type::*; + + let tl = self.type_expr(left)?; + let tr = self.type_expr(right)?; + + Ok(match (tl, op, tr) { + (Str, Add, Str) => Str, + (Int, Add, Int) => Int, + (Array(t1), Add, Array(t2)) => Array(Box::new(t1.unify(*t2, Coerce::Common)?)), + (Int, Sub, Int) => Int, + (Array(t1), Sub, Array(t2)) => { + (*t1).clone().unify(*t2, Coerce::Compare)?; + Array(t1) + } + (Int, Mul, Int) => Int, + (Int, Div, Int) => Int, + (Int, Rem, Int) => Int, + (Bool, And, Bool) => Bool, + (Bool, Or, Bool) => Bool, + (l, Eq, r) => { + l.unify(r, Coerce::Compare)?; + Bool + } + (l, Ne, r) => { + l.unify(r, Coerce::Compare)?; + Bool + } + (Int, Lt, Int) => Bool, + (Int, Le, Int) => Bool, + (Int, Ge, Int) => Bool, + (Int, Gt, Int) => Bool, + (Str, Lt, Str) => Bool, + (Str, Le, Str) => Bool, + (Str, Ge, Str) => Bool, + (Str, Gt, Str) => Bool, + _ => return Err(Error::typ("invalid types for operation")), + }) + } + + fn type_unary_op(&mut self, op: expr::OpUnary, expr: &expr::Expr<'_>) -> Result<Type> { + use expr::OpUnary::*; + use Type::*; + + let typ = self.type_expr(expr)?; + + Ok(match (op, typ) { + (Not, Bool) => Bool, + (Neg, Int) => Int, + _ => return Err(Error::typ("invalid type for operation")), + }) + } + + fn type_index(&mut self, base: &expr::Expr<'_>, index: &expr::Expr<'_>) -> Result<Type> { + use Type::*; + + let base_type = self.type_expr(base)?; + let index_type = self.type_expr(index)?; + + if let Array(elem_type) = base_type { + if index_type == Int { + return Ok(*elem_type); + } + } else if let Map(key_type, value_type) = base_type { + if Type::from(key_type) + .unify(index_type, Coerce::Assign) + .is_ok() + { + return Ok(*value_type); + } + } + + Err(Error::typ("invalid types index operation")) + } + + fn type_func(&mut self, func: FuncType, call_param_types: Vec<Result<Type>>) -> Result<Type> { + if func.params.len() != call_param_types.len() { + return Err(Error::typ("incorrect number of parameters")); + } + + for (func_param_type, call_param_type) in func.params.iter().zip(call_param_types) { + func_param_type + .clone() + .unify(call_param_type?, Coerce::Assign)?; + } + + Ok(func.ret) + } + + fn type_apply(&mut self, expr: &expr::Expr<'_>, params: &[expr::Expr]) -> Result<Type> { + use Type::*; + + let expr_type = self.type_expr(expr)?; + + let Fn(func) = expr_type else { + return Err(Error::typ("invalid type for function call")); + }; + + let param_types = params.iter().map(|param| self.type_expr(param)).collect(); + + self.type_func(*func, param_types) + } + + fn type_method( + &mut self, + expr: &expr::Expr<'_>, + method: &ast::Ident<'_>, + params: &[expr::Expr], + ) -> Result<Type> { + let self_type = self.type_expr(expr)?; + let type_family = TypeFamily::from(&self_type); + + let method = self + .0 + .methods + .get(&type_family) + .and_then(|methods| methods.get(method.name.as_ref())) + .ok_or(Error::lookup("undefined method"))? + .typ + .clone(); + + let param_types = iter::once(Ok(self_type)) + .chain(params.iter().map(|param| self.type_expr(param))) + .collect(); + + self.type_func(method, param_types) + } + + fn type_field(&mut self, base: &expr::Expr<'_>, field: &ast::Ident<'_>) -> Result<Type> { + use Type::*; + + let base_type = self.type_expr(base)?; + let name = field.name.as_ref(); + + Ok(match base_type { + Tuple(elems) => { + let index: usize = name.parse().or(Err(Error::typ("no such field")))?; + elems + .into_iter() + .nth(index) + .ok_or(Error::typ("no such field"))? + } + Struct(mut entries) => entries.remove(name).ok_or(Error::typ("no such field"))?, + _ => return Err(Error::typ("invalid field access base type")), + }) + } + + fn type_scope(&mut self, stmts: &[ast::BlockStmt]) -> Result<(Type, FxHashSet<String>)> { + let (ret, upvalues) = self.scoped(|ctx| { + let mut ret = Type::unit(); + for stmt in stmts { + ret = ctx.type_block_stmt(stmt)?; + } + Ok(ret) + }); + Ok((ret?, upvalues)) + } + + fn type_block(&mut self, block: &ast::Block) -> Result<Type> { + if let [ast::BlockStmt::Expr { expr }] = &block.0[..] { + return self.type_expr(expr); + } + + let (ret, upvalues) = self.type_scope(&block.0)?; + self.0.initialize_all(upvalues); + Ok(ret) + } + + fn type_ifelse( + &mut self, + if_blocks: &[(expr::Expr, ast::Block)], + else_block: &Option<Box<ast::Block>>, + ) -> Result<Type> { + let (mut ret, mut common_upvalues) = if let Some(block) = else_block { + self.type_scope(&block.0)? + } else { + (Type::unit(), FxHashSet::default()) + }; + + for (cond, block) in if_blocks { + Type::Bool.unify(self.type_expr(cond)?, Coerce::Assign)?; + + let (typ, upvalues) = self.type_scope(&block.0)?; + ret = ret.unify(typ, Coerce::Common)?; + common_upvalues = &common_upvalues & &upvalues; + } + + self.0.initialize_all(common_upvalues); + + Ok(ret) + } + + fn type_path(&self, path: &ast::Path<'_>) -> Result<Type> { + let var = self.0.lookup_value(path)?; + if !self.0.is_initialized(path) { + return Err(Error::typ("uninitialized variable")); + } + Ok(var.inferred_type.clone()) + } + + fn check_string_interp_type(typ: Type, kind: expr::StrKind) -> Result<()> { + match (typ, kind) { + (Type::Free, _) => Ok(()), + (Type::Bool, _) => Ok(()), + (Type::Int, _) => Ok(()), + (Type::Str, _) => Ok(()), + (Type::Option(inner), expr::StrKind::Script) => { + Self::check_string_interp_type(*inner, kind) + } + (Type::Tuple(elems), expr::StrKind::Script) => { + for elem in elems { + Self::check_string_interp_type(elem, kind)?; + } + Ok(()) + } + (Type::Array(inner), expr::StrKind::Script) => { + Self::check_string_interp_type(*inner, kind) + } + _ => Err(Error::typ("invalid type for string interpolation")), + } + } + + fn check_string_piece(&mut self, piece: &expr::StrPiece, kind: expr::StrKind) -> Result<()> { + let typ = match piece { + expr::StrPiece::Chars(_) => return Ok(()), + expr::StrPiece::Escape(_) => return Ok(()), + expr::StrPiece::Interp(expr) => self.type_expr(expr)?, + }; + Self::check_string_interp_type(typ, kind) + } + + fn type_literal(&mut self, lit: &expr::Literal<'_>) -> Result<Type> { + use expr::Literal; + use Type::*; + + Ok(match lit { + Literal::Unit => Type::unit(), + Literal::None => Type::Option(Box::new(Type::Free)), + Literal::Bool(_) => Bool, + Literal::Int(_) => Int, + Literal::Str { pieces, kind } => { + for piece in pieces { + self.check_string_piece(piece, *kind)?; + } + Str + } + Literal::Tuple(elems) => Tuple( + elems + .iter() + .map(|elem| self.type_expr(elem)) + .collect::<Result<_>>()?, + ), + Literal::Array(elems) => Array(Box::new( + elems.iter().try_fold(Type::Free, |acc, elem| { + acc.unify(self.type_expr(elem)?, Coerce::Common) + })?, + )), + Literal::Map(entries) => { + let (key, value) = entries.iter().try_fold( + (Type::Free, Type::Free), + |(acc_key, acc_value), expr::MapEntry { key, value }| { + Ok(( + acc_key.unify(self.type_expr(key)?, Coerce::Common)?, + acc_value.unify(self.type_expr(value)?, Coerce::Common)?, + )) + }, + )?; + Type::Map(key.try_into()?, Box::new(value)) + } + Literal::Struct(entries) => { + if entries.is_empty() { + return Ok(Type::unit()); + } + Struct( + entries + .iter() + .map(|expr::StructField { name, value }| { + Ok((name.as_ref().to_owned(), self.type_expr(value)?)) + }) + .collect::<Result<_>>()?, + ) + } + }) + } + + #[allow(clippy::type_complexity)] + fn assign_destr_pat_type_with<'a, 'r, R: 'r>( + &mut self, + pat: &pat::DestrPat<'a>, + f: Box<dyn FnOnce(&mut Type, ast::Ident<'a>) -> Result<R> + 'r>, + ) -> Result<R> { + let initialized = self.0.initialized.clone(); + match pat { + pat::DestrPat::Index { base, index } => { + let index_type = self.type_expr(index)?; + self.assign_destr_pat_type_with( + base, + Box::new(|base_type, ident| { + if !initialized.contains(ident.name.as_ref()) { + return Err(Error::typ( + "tried to assign field of uninitialized variable", + )); + } + match (base_type, index_type) { + (Type::Array(inner), Type::Int) => f(inner.as_mut(), ident), + (Type::Map(key_type, value_type), index_type) => { + *key_type = key_type + .clone() + .unify(OrdType::try_from(index_type)?, Coerce::Common)?; + f(value_type.as_mut(), ident) + } + _ => Err(Error::typ("invalid types for index operation")), + } + }), + ) + } + pat::DestrPat::Field { base, field } => self.assign_destr_pat_type_with( + base, + Box::new(|typ, ident| { + if !initialized.contains(ident.name.as_ref()) { + return Err(Error::typ( + "tried to assign field of uninitialized variable", + )); + } + match typ { + Type::Tuple(elems) => { + let index: Option<usize> = field.name.parse().ok(); + f( + index + .and_then(|index| elems.get_mut(index)) + .ok_or(Error::typ("no such field"))?, + ident, + ) + } + Type::Struct(entries) => f( + entries + .get_mut(field.name.as_ref()) + .ok_or(Error::typ("no such field"))?, + ident, + ), + _ => Err(Error::typ("invalid field access base type")), + } + }), + ), + pat::DestrPat::Paren(subpat) => self.assign_destr_pat_type_with(subpat, f), + pat::DestrPat::Path(path) => { + let (var, ident) = self.0.lookup_var_mut(path)?; + f(&mut var.inferred_type, ident) + } + } + } + + fn assign_destr_pat_type(&mut self, dest: &pat::DestrPat, typ: Type) -> Result<Type> { + if scope::is_wildcard_destr_pat(dest) { + return Ok(Type::unit()); + } + + let (inferred_type, ident) = self.assign_destr_pat_type_with( + dest, + Box::new(|dest_type, ident| { + let inferred_type = dest_type.clone().unify(typ, Coerce::Common)?; + *dest_type = inferred_type.clone(); + + Ok((inferred_type, ident)) + }), + )?; + + self.0.initialize(ident.name.as_ref()); + + // TODO: Check explicit type + + Ok(inferred_type) + } + + fn scoped<F, R>(&mut self, f: F) -> (R, FxHashSet<String>) + where + F: FnOnce(&mut Context) -> R, + { + self.0.scoped(|scope| f(&mut Context(scope))) + } +} + +impl<'scope, T> TypeContext<'scope, T> { + pub fn type_type(&self, typ: &typ::Type<'_>) -> Result<Type> { + use typ::Type::*; + + Ok(match typ { + Paren(subtyp) => self.type_type(subtyp)?, + Option(subtyp) => Type::Option(Box::new(self.type_type(subtyp)?)), + Path(path) => self.type_type_path(path)?, + Literal(lit) => self.type_type_literal(lit)?, + }) + } + + fn type_type_path(&self, path: &ast::Path<'_>) -> Result<Type> { + self.0.lookup_type(path).cloned() + } + + fn type_type_literal(&self, lit: &typ::Literal<'_>) -> Result<Type> { + use typ::Literal; + use Type::*; + + Ok(match lit { + Literal::Unit => Type::unit(), + Literal::Tuple(elems) => Tuple( + elems + .iter() + .map(|elem| self.type_type(elem)) + .collect::<Result<_>>()?, + ), + Literal::Array(typ) => Array(Box::new(self.type_type(typ)?)), + Literal::Map(key, value) => Map( + self.type_type(key)?.try_into()?, + Box::new(self.type_type(value)?), + ), + Literal::Struct(entries) => { + if entries.is_empty() { + return Ok(Type::unit()); + } + Struct( + entries + .iter() + .map(|typ::StructField { name, typ }| { + Ok((name.as_ref().to_owned(), self.type_type(typ)?)) + }) + .collect::<Result<_>>()?, + ) + } + }) + } + + pub fn type_fn_def( + &self, + params: &[ast::FuncParam], + ret: Option<&typ::Type>, + ) -> Result<FuncType> { + let param_types: Vec<_> = params + .iter() + .map(|param| self.type_type(¶m.typ)) + .collect::<Result<_>>()?; + let ret_type = ret + .map(|typ| self.type_type(typ)) + .transpose()? + .unwrap_or(Type::unit()); + + Ok(FuncType { + params: param_types, + ret: ret_type, + }) + } +} + +impl Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Type::Free => f.write_str("_"), + Type::Bool => f.write_str("bool"), + Type::Int => f.write_str("int"), + Type::Str => f.write_str("str"), + Type::Option(inner) => write!(f, "{inner}?"), + Type::Tuple(elems) => { + let mut first = true; + f.write_str("(")?; + for elem in elems { + if !first { + f.write_str(", ")?; + } + first = false; + elem.fmt(f)?; + } + if elems.len() == 1 { + f.write_str(",")?; + } + f.write_str(")") + } + Type::Array(typ) => write!(f, "[{typ}]"), + Type::Map(key, value) => write!(f, "map{{{key} => {value}}}"), + Type::Struct(entries) => { + let mut first = true; + f.write_str("{")?; + for (key, typ) in entries { + if !first { + f.write_str(", ")?; + } + first = false; + write!(f, "{key}: {typ}")?; + } + f.write_str("}") + } + /* TODO */ + Type::Fn(func) => write!(f, "{func}"), + } + } +} diff --git a/crates/rebel-lang/src/value.rs b/crates/rebel-lang/src/value.rs new file mode 100644 index 0000000..bca3fee --- /dev/null +++ b/crates/rebel-lang/src/value.rs @@ -0,0 +1,713 @@ +use std::{ + cell::RefCell, + fmt::{Display, Write}, + iter, +}; + +use rebel_parse::ast::{self, expr, pat, typ}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + func::{Func, FuncDef}, + scope::{self, Scope}, + typing::{Coerce, OrdType, Type, TypeContext, TypeFamily}, + Error, Result, +}; + +#[derive(Debug)] +pub struct Context<'scope>(pub &'scope mut Box<Scope<Value>>); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Value { + Uninitialized, + None, + Bool(bool), + Int(i64), + Str(String), + Tuple(Vec<Value>), + Map(FxHashMap<OrdValue, Value>), + Array(Vec<Value>), + Struct(FxHashMap<String, Value>), + Fn(Box<Func>), +} + +impl Value { + pub fn unit() -> Self { + Value::Tuple(Vec::new()) + } + + pub fn typ(&self) -> Result<Type> { + Ok(match self { + Value::Uninitialized => return Err(Error::typ("uninitialized value")), + Value::None => Type::Option(Box::new(Type::Free)), + Value::Bool(_) => Type::Bool, + Value::Int(_) => Type::Int, + Value::Str(_) => Type::Str, + Value::Tuple(elems) => { + Type::Tuple(elems.iter().map(|elem| elem.typ()).collect::<Result<_>>()?) + } + Value::Array(elems) => Type::Array(Box::new(Self::common_type(elems)?)), + Value::Map(entries) => Type::Map( + Self::common_ord_type(entries.keys())?, + Box::new(Self::common_type(entries.values())?), + ), + Value::Struct(entries) => Type::Struct( + entries + .iter() + .map(|(k, v)| Ok((k.clone(), v.typ()?))) + .collect::<Result<_>>()?, + ), + Value::Fn(func) => Type::Fn(Box::new(func.typ.clone())), + }) + } + + fn common_type<'a>(values: impl IntoIterator<Item = &'a Value>) -> Result<Type> { + values.into_iter().try_fold(Type::Free, |acc, value| { + acc.unify(value.typ()?, Coerce::Common) + }) + } + + fn common_ord_type<'a>(values: impl IntoIterator<Item = &'a OrdValue>) -> Result<OrdType> { + values.into_iter().try_fold(OrdType::Free, |acc, value| { + acc.unify(value.typ()?, Coerce::Common) + }) + } +} + +impl From<OrdValue> for Value { + fn from(value: OrdValue) -> Self { + match value { + OrdValue::Int(v) => Value::Int(v), + OrdValue::Str(v) => Value::Str(v), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum OrdValue { + Int(i64), + Str(String), +} + +impl OrdValue { + pub fn typ(&self) -> Result<OrdType> { + Ok(match self { + OrdValue::Int(_) => OrdType::Int, + OrdValue::Str(_) => OrdType::Str, + }) + } +} + +impl Display for OrdValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OrdValue::Int(value) => value.fmt(f), + OrdValue::Str(value) => write!(f, "{value:?}"), + } + } +} + +impl TryFrom<Value> for OrdValue { + type Error = Error; + + fn try_from(value: Value) -> Result<Self> { + Ok(match value { + Value::Int(v) => OrdValue::Int(v), + Value::Str(v) => OrdValue::Str(v), + _ => return Err(Error::bug("OrdValue from unordered value")), + }) + } +} + +impl<'scope> Context<'scope> { + pub fn eval_block_stmt(&mut self, stmt: &ast::BlockStmt) -> Result<Value> { + Ok(match stmt { + ast::BlockStmt::Let { dest, expr } => { + let ast::TypedPat { pat, typ: _ } = dest.as_ref(); + + let dest_ident = scope::pat_ident(pat); + + let value = expr.as_ref().map(|expr| self.eval_expr(expr)).transpose()?; + + if dest_ident.name == "_" { + return Ok(Value::unit()); + } + + self.0 + .defs + .insert_value(dest_ident.name.as_ref(), Value::Uninitialized); + + let Some(value) = value else { + return Ok(Value::unit()); + }; + + self.assign_destr_pat_value(&pat::DestrPat::from(pat.borrowed()), value)? + } + ast::BlockStmt::Assign { dest, expr } => { + let value = self.eval_expr(expr)?; + self.assign_destr_pat_value(dest, value)? + } + ast::BlockStmt::Fn { + ident, + params, + ret, + block, + } => { + let value = Value::Fn(Box::new(self.eval_fn_def(params, ret.as_deref(), block)?)); + + // TODO: Reject in validation? + if ident.name == "_" { + return Ok(Value::unit()); + } + self.0.defs.insert_value(ident.name.as_ref(), value.clone()); + + value + } + ast::BlockStmt::Expr { expr } => self.eval_expr(expr)?, + ast::BlockStmt::Empty => Value::unit(), + }) + } + + fn eval_fn_def( + &mut self, + params: &[ast::FuncParam], + ret: Option<&typ::Type>, + block: &ast::Block, + ) -> Result<Func> { + let type_ctx = TypeContext(self.0); + let typ = type_ctx.type_fn_def(params, ret)?; + + let param_names: Vec<_> = params + .iter() + .map(|param| param.name.name.as_ref().to_owned()) + .collect(); + + Ok(Func { + typ, + def: FuncDef::Body { + param_names, + block: block.borrowed().into_owned(), + }, + }) + } + + pub fn eval_expr(&mut self, expr: &expr::Expr<'_>) -> Result<Value> { + use expr::Expr::*; + + match expr { + Binary { left, op, right } => self.eval_binary_op(left, *op, right), + Unary { op, expr } => self.eval_unary_op(*op, expr), + Apply { expr, params } => self.eval_apply(expr, params), + Method { + expr, + method, + params, + } => self.eval_method(expr, method, params), + Index { base, index } => self.eval_index(base, index), + Field { base, field } => self.eval_field(base, field), + Block(block) => self.eval_block(block), + IfElse { + if_blocks, + else_block, + } => self.eval_ifelse(if_blocks, else_block), + Paren(subexpr) => self.eval_expr(subexpr), + Path(path) => self.eval_path(path), + Literal(lit) => self.eval_literal(lit), + } + } + + fn eval_binary_op( + &mut self, + left: &expr::Expr<'_>, + op: expr::OpBinary, + right: &expr::Expr<'_>, + ) -> Result<Value> { + use expr::OpBinary::*; + use Value::*; + + let tl = self.eval_expr(left)?; + let tr = self.eval_expr(right)?; + + Ok(match (tl, op, tr) { + (Str(s1), Add, Str(s2)) => Str(s1 + &s2), + (Int(i1), Add, Int(i2)) => Int(i1 + .checked_add(i2) + .ok_or(Error::eval("integer over- or underflow"))?), + (Array(elems1), Add, Array(elems2)) => Array([elems1, elems2].concat()), + (Int(i1), Sub, Int(i2)) => Int(i1 + .checked_sub(i2) + .ok_or(Error::eval("integer over- or underflow"))?), + (Array(elems1), Sub, Array(elems2)) => Array( + elems1 + .into_iter() + .filter(|elem| !elems2.contains(elem)) + .collect(), + ), + (Int(i1), Mul, Int(i2)) => Int(i1 + .checked_mul(i2) + .ok_or(Error::eval("integer over- or underflow"))?), + (Int(i1), Div, Int(i2)) => { + Int(i1.checked_div(i2).ok_or(Error::eval("division by zero"))?) + } + (Int(i1), Rem, Int(i2)) => { + Int(i1.checked_rem(i2).ok_or(Error::eval("division by zero"))?) + } + (Bool(b1), And, Bool(b2)) => Bool(b1 && b2), + (Bool(b1), Or, Bool(b2)) => Bool(b1 || b2), + (l, Eq, r) => Bool(l == r), + (l, Ne, r) => Bool(l != r), + (Int(i1), Lt, Int(i2)) => Bool(i1 < i2), + (Int(i1), Le, Int(i2)) => Bool(i1 <= i2), + (Int(i1), Ge, Int(i2)) => Bool(i1 >= i2), + (Int(i1), Gt, Int(i2)) => Bool(i1 > i2), + (Str(i1), Lt, Str(i2)) => Bool(i1 < i2), + (Str(i1), Le, Str(i2)) => Bool(i1 <= i2), + (Str(i1), Ge, Str(i2)) => Bool(i1 >= i2), + (Str(i1), Gt, Str(i2)) => Bool(i1 > i2), + _ => return Err(Error::typ("invalid types for operation")), + }) + } + + fn eval_unary_op(&mut self, op: expr::OpUnary, expr: &expr::Expr<'_>) -> Result<Value> { + use expr::OpUnary::*; + use Value::*; + + let typ = self.eval_expr(expr)?; + + Ok(match (op, typ) { + (Not, Bool(val)) => Bool(!val), + (Neg, Int(val)) => Int(val + .checked_neg() + .ok_or(Error::eval("integer over- or underflow"))?), + _ => return Err(Error::typ("invalid type for operation")), + }) + } + + fn eval_index(&mut self, base: &expr::Expr<'_>, index: &expr::Expr<'_>) -> Result<Value> { + use Value::*; + + let base_value = self.eval_expr(base)?; + let index_value = self.eval_expr(index)?; + + if let Array(elems) = base_value { + if let Int(index) = index_value { + return usize::try_from(index) + .ok() + .and_then(|index| elems.into_iter().nth(index)) + .ok_or(Error::eval("array index out of bounds")); + } + } else if let Map(entries) = base_value { + return entries + .get(&index_value.try_into()?) + .cloned() + .ok_or(Error::eval("map key not found")); + } + + Err(Error::typ("invalid types index operation")) + } + + fn eval_func(&mut self, func: Func, call_param_values: Vec<Value>) -> Result<Value> { + match func.def { + FuncDef::Intrinsic(f) => f(&call_param_values), + FuncDef::Body { param_names, block } => { + let mut scope = Box::new(self.0.func()); + + for (param_name, param_value) in param_names.iter().zip(call_param_values) { + scope.defs.insert_value(param_name, param_value); + } + + Context(&mut scope).eval_block(&block) + } + } + } + + fn eval_apply(&mut self, expr: &expr::Expr<'_>, params: &[expr::Expr]) -> Result<Value> { + use Value::*; + + let value = self.eval_expr(expr)?; + + let Fn(func) = value else { + return Err(Error::typ("invalid type for function call")); + }; + + let param_values: Vec<_> = params + .iter() + .map(|param| self.eval_expr(param)) + .collect::<Result<_>>()?; + + self.eval_func(*func, param_values) + } + + fn eval_method( + &mut self, + expr: &expr::Expr<'_>, + method: &ast::Ident<'_>, + params: &[expr::Expr], + ) -> Result<Value> { + let self_value = self.eval_expr(expr)?; + // TODO: Use inferred type for method lookup + let self_type = self_value.typ()?; + let type_family = TypeFamily::from(&self_type); + + let method = self + .0 + .methods + .get(&type_family) + .and_then(|methods| methods.get(method.name.as_ref())) + .ok_or(Error::lookup("undefined method"))? + .clone(); + + let param_values: Vec<_> = iter::once(Ok(self_value)) + .chain(params.iter().map(|param| self.eval_expr(param))) + .collect::<Result<_>>()?; + + self.eval_func(method, param_values) + } + + fn eval_field(&mut self, base: &expr::Expr<'_>, field: &ast::Ident<'_>) -> Result<Value> { + use Value::*; + + let base_value = self.eval_expr(base)?; + let name = field.name.as_ref(); + + Ok(match base_value { + Tuple(elems) => { + let index: usize = name.parse().or(Err(Error::typ("no such field")))?; + elems + .into_iter() + .nth(index) + .ok_or(Error::typ("no such field"))? + } + Struct(mut entries) => entries.remove(name).ok_or(Error::typ("no such field"))?, + _ => return Err(Error::typ("invalid field access base type")), + }) + } + + fn eval_scope(&mut self, stmts: &[ast::BlockStmt]) -> Result<(Value, FxHashSet<String>)> { + let (ret, upvalues) = self.scoped(|ctx| { + let mut ret = Value::unit(); + for stmt in stmts { + ret = ctx.eval_block_stmt(stmt)?; + } + Ok(ret) + }); + Ok((ret?, upvalues)) + } + + fn eval_block(&mut self, block: &ast::Block) -> Result<Value> { + if let [ast::BlockStmt::Expr { expr }] = &block.0[..] { + return self.eval_expr(expr); + } + + let (ret, upvalues) = self.eval_scope(&block.0)?; + self.0.initialize_all(upvalues); + Ok(ret) + } + + fn eval_ifelse( + &mut self, + if_blocks: &[(expr::Expr, ast::Block)], + else_block: &Option<Box<ast::Block>>, + ) -> Result<Value> { + for (cond, block) in if_blocks { + if self.eval_expr(cond)? == Value::Bool(true) { + return self.eval_block(block); + } + } + if let Some(block) = else_block { + self.eval_block(block) + } else { + Ok(Value::unit()) + } + } + + fn eval_path(&self, path: &ast::Path<'_>) -> Result<Value> { + Ok(self.0.lookup_value(path)?.clone()) + } + + fn eval_literal(&mut self, lit: &expr::Literal<'_>) -> Result<Value> { + use expr::Literal; + use Value::*; + + Ok(match lit { + Literal::Unit => Value::unit(), + Literal::None => Value::None, + Literal::Bool(val) => Bool(*val), + Literal::Int(val) => Int(*val), + Literal::Str { pieces, kind } => Str(StrDisplay { + pieces, + kind: *kind, + ctx: RefCell::new(self), + } + .to_string()), + Literal::Tuple(elems) => Tuple( + elems + .iter() + .map(|elem| self.eval_expr(elem)) + .collect::<Result<_>>()?, + ), + Literal::Array(elems) => Array( + elems + .iter() + .map(|elem| self.eval_expr(elem)) + .collect::<Result<_>>()?, + ), + Literal::Map(entries) => { + let map: FxHashMap<_, _> = entries + .iter() + .map(|expr::MapEntry { key, value }| { + Ok((self.eval_expr(key)?.try_into()?, self.eval_expr(value)?)) + }) + .collect::<Result<_>>()?; + if map.len() != entries.len() { + return Err(Error::eval("duplicate map key")); + } + Map(map) + } + Literal::Struct(entries) => { + if entries.is_empty() { + return Ok(Value::unit()); + } + Struct( + entries + .iter() + .map(|expr::StructField { name, value }| { + Ok((name.as_ref().to_owned(), self.eval_expr(value)?)) + }) + .collect::<Result<_>>()?, + ) + } + }) + } + + #[allow(clippy::type_complexity)] + fn assign_destr_pat_value_with<'r, R: 'r>( + &mut self, + pat: &pat::DestrPat, + f: Box<dyn FnOnce(&mut Value) -> Result<R> + 'r>, + ) -> Result<R> { + match pat { + pat::DestrPat::Index { base, index } => { + let index_value = self.eval_expr(index)?; + self.assign_destr_pat_value_with( + base, + Box::new(|base_value| match (base_value, index_value) { + (Value::Array(inner), Value::Int(index)) => f(usize::try_from(index) + .ok() + .and_then(|index| inner.get_mut(index)) + .ok_or(Error::eval("array index out of bounds"))?), + (Value::Map(entries), key) => { + use std::collections::hash_map::Entry; + match entries.entry(OrdValue::try_from(key)?) { + Entry::Occupied(mut entry) => f(entry.get_mut()), + Entry::Vacant(entry) => { + let mut tmp = Value::Uninitialized; + let ret = + f(&mut tmp).or(Err(Error::eval("map key not found")))?; + entry.insert(tmp); + Ok(ret) + } + } + } + _ => Err(Error::typ("invalid types for index operation")), + }), + ) + } + pat::DestrPat::Field { base, field } => self.assign_destr_pat_value_with( + base, + Box::new(|value| match value { + Value::Tuple(elems) => { + let index: Option<usize> = field.name.parse().ok(); + f(index + .and_then(|index| elems.get_mut(index)) + .ok_or(Error::typ("no such field"))?) + } + Value::Struct(value_entries) => f(value_entries + .get_mut(field.name.as_ref()) + .ok_or(Error::typ("no such field"))?), + _ => Err(Error::typ("invalid field access base type")), + }), + ), + pat::DestrPat::Paren(subpat) => self.assign_destr_pat_value_with(subpat, f), + pat::DestrPat::Path(path) => f(self.0.lookup_var_mut(path)?.0), + } + } + + fn assign_destr_pat_value(&mut self, dest: &pat::DestrPat, value: Value) -> Result<Value> { + if scope::is_wildcard_destr_pat(dest) { + return Ok(Value::unit()); + } + + self.assign_destr_pat_value_with( + dest, + Box::new(|dest_value| { + *dest_value = value.clone(); + Ok(()) + }), + )?; + + Ok(value) + } + + fn scoped<F, R>(&mut self, f: F) -> (R, FxHashSet<String>) + where + F: FnOnce(&mut Context) -> R, + { + self.0.scoped(|scope| f(&mut Context(scope))) + } +} + +impl Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Uninitialized => f.write_str("_"), + Value::None => f.write_str("none"), + Value::Bool(value) => value.fmt(f), + Value::Int(value) => value.fmt(f), + Value::Str(value) => write!(f, "{value:?}"), + Value::Tuple(elems) => { + let mut first = true; + f.write_str("(")?; + for elem in elems { + if !first { + f.write_str(", ")?; + } + first = false; + elem.fmt(f)?; + } + if elems.len() == 1 { + f.write_str(",")?; + } + f.write_str(")") + } + Value::Array(elems) => { + let mut first = true; + f.write_str("[")?; + for elem in elems { + if !first { + f.write_str(", ")?; + } + first = false; + elem.fmt(f)?; + } + f.write_str("]") + } + Value::Map(entries) => { + let mut first = true; + let mut entries: Vec<_> = entries.iter().collect(); + entries.sort_unstable_by_key(|(key, _)| *key); + f.write_str("map{")?; + for (key, value) in entries { + if !first { + f.write_str(", ")?; + } + first = false; + write!(f, "{key} => {value}")?; + } + f.write_str("}") + } + Value::Struct(entries) => { + let mut first = true; + f.write_str("{")?; + for (key, value) in entries { + if !first { + f.write_str(", ")?; + } + first = false; + write!(f, "{key}: {value}")?; + } + f.write_str("}") + } + Value::Fn(func) => func.typ.fmt(f), + } + } +} + +#[derive(Debug)] +struct Stringify<'a>(&'a Value); + +impl<'a> Display for Stringify<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Value::Bool(value) => value.fmt(f), + Value::Int(value) => value.fmt(f), + Value::Str(value) => value.fmt(f), + _ => Err(std::fmt::Error), + } + } +} + +#[derive(Debug)] +struct ScriptStringify<'a>(&'a Value); + +impl<'a> ScriptStringify<'a> { + fn fmt_list(elems: &'a [Value], f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for elem in elems { + if !first { + f.write_char(' ')?; + } + ScriptStringify(elem).fmt(f)?; + first = false; + } + Ok(()) + } +} + +impl<'a> Display for ScriptStringify<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Value::Bool(value) => { + value.fmt(f)?; + } + Value::Int(value) => { + value.fmt(f)?; + } + Value::Str(value) => { + f.write_char('\'')?; + f.write_str(&value.replace('\'', "'\\''"))?; + f.write_char('\'')?; + } + Value::None => {} + Value::Array(elems) => { + Self::fmt_list(elems, f)?; + } + Value::Tuple(elems) => { + Self::fmt_list(elems, f)?; + } + _ => return Err(std::fmt::Error), + }; + Ok(()) + } +} + +#[derive(Debug)] +struct StrDisplay<'a, 'scope> { + pieces: &'a [expr::StrPiece<'a>], + kind: expr::StrKind, + ctx: RefCell<&'a mut Context<'scope>>, +} + +impl<'a, 'scope> Display for StrDisplay<'a, 'scope> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for piece in self.pieces { + match piece { + expr::StrPiece::Chars(chars) => f.write_str(chars)?, + expr::StrPiece::Escape(c) => f.write_char(*c)?, + expr::StrPiece::Interp(expr) => { + let val = self + .ctx + .borrow_mut() + .eval_expr(expr) + .or(Err(std::fmt::Error))?; + match self.kind { + expr::StrKind::Regular => Stringify(&val).fmt(f), + expr::StrKind::Raw => unreachable!(), + expr::StrKind::Script => ScriptStringify(&val).fmt(f), + }?; + } + }; + } + Ok(()) + } +} diff --git a/crates/rebel-parse/Cargo.toml b/crates/rebel-parse/Cargo.toml index 0233af8..d116736 100644 --- a/crates/rebel-parse/Cargo.toml +++ b/crates/rebel-parse/Cargo.toml @@ -8,8 +8,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -peg = { git = "https://github.com/kevinmehall/rust-peg.git", version = "0.8.2" } +derive-into-owned = { git = "https://github.com/neocturne/derive-into-owned.git", branch = "more-types" } +peg = "0.8.3" +phf = { version = "0.11.2", features = ["macros"] } rebel-common = { path = "../rebel-common" } +rustc-hash = "1.1.0" [dev-dependencies] clap = { version = "4.0.0", features = ["derive"] } diff --git a/crates/rebel-parse/examples/parse-string.rs b/crates/rebel-parse/examples/parse-string.rs index 190d0fa..9750a87 100644 --- a/crates/rebel-parse/examples/parse-string.rs +++ b/crates/rebel-parse/examples/parse-string.rs @@ -9,9 +9,11 @@ enum Rule { Tokenize, Recipe, RecipeStmt, - Body, - BodyStmt, + Block, + BlockStmt, Expr, + Type, + Pat, } #[derive(Clone, Debug, Parser)] @@ -46,9 +48,11 @@ fn main() { Rule::Tokenize => Ok(as_debug(tokens)), Rule::Recipe => recipe::recipe(&tokens).map(as_debug), Rule::RecipeStmt => recipe::recipe_stmt(&tokens).map(as_debug), - Rule::Body => recipe::body(&tokens).map(as_debug), - Rule::BodyStmt => recipe::body_stmt(&tokens).map(as_debug), + Rule::Block => recipe::block(&tokens).map(as_debug), + Rule::BlockStmt => recipe::block_stmt(&tokens).map(as_debug), Rule::Expr => recipe::expr(&tokens).map(as_debug), + Rule::Type => recipe::typ(&tokens).map(as_debug), + Rule::Pat => recipe::pat(&tokens).map(as_debug), }; if opts.rule != Rule::Tokenize { let dur = Instant::now().duration_since(start); diff --git a/crates/rebel-parse/src/ast.rs b/crates/rebel-parse/src/ast.rs deleted file mode 100644 index 648a79e..0000000 --- a/crates/rebel-parse/src/ast.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::token; - -pub type Recipe<'a> = Vec<RecipeStmt<'a>>; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RecipeStmt<'a> { - BodyStmt(BodyStmt<'a>), - Fetch { - name: Ident<'a>, - body: Body<'a>, - }, - Task { - name: Ident<'a>, - params: Vec<FuncParam<'a>>, - body: Body<'a>, - }, -} - -pub type Body<'a> = Vec<BodyStmt<'a>>; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum BodyStmt<'a> { - Assign { - left: Box<TypedExpr<'a>>, - op: Option<OpBinary>, - right: Box<Expr<'a>>, - }, -} - -impl<'a> BodyStmt<'a> { - pub(crate) fn assign(left: TypedExpr<'a>, op: Option<OpBinary>, right: Expr<'a>) -> Self { - BodyStmt::Assign { - left: Box::new(left), - op, - right: Box::new(right), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Expr<'a> { - Binary { - left: Box<Expr<'a>>, - op: OpBinary, - right: Box<Expr<'a>>, - }, - Unary { - op: OpUnary, - expr: Box<Expr<'a>>, - }, - Apply { - expr: Box<Expr<'a>>, - params: Vec<Expr<'a>>, - }, - Method { - expr: Box<Expr<'a>>, - method: Ident<'a>, - params: Vec<Expr<'a>>, - }, - Index { - expr: Box<Expr<'a>>, - index: Box<Expr<'a>>, - }, - Field { - expr: Box<Expr<'a>>, - field: Ident<'a>, - }, - Paren(Box<Expr<'a>>), - Path(Path<'a>), - Literal(Literal<'a>), -} - -impl<'a> Expr<'a> { - pub(crate) fn binary(left: Expr<'a>, op: OpBinary, right: Expr<'a>) -> Self { - Expr::Binary { - left: Box::new(left), - op, - right: Box::new(right), - } - } - - pub(crate) fn unary(op: OpUnary, expr: Expr<'a>) -> Self { - Expr::Unary { - op, - expr: Box::new(expr), - } - } - - pub(crate) fn apply(expr: Expr<'a>, params: Vec<Expr<'a>>) -> Self { - Expr::Apply { - expr: Box::new(expr), - params, - } - } - - pub(crate) fn method(expr: Expr<'a>, method: Ident<'a>, params: Vec<Expr<'a>>) -> Self { - Expr::Method { - expr: Box::new(expr), - method, - params, - } - } - - pub(crate) fn index(expr: Expr<'a>, index: Expr<'a>) -> Self { - Expr::Index { - expr: Box::new(expr), - index: Box::new(index), - } - } - - pub(crate) fn field(expr: Expr<'a>, field: Ident<'a>) -> Self { - Expr::Field { - expr: Box::new(expr), - field, - } - } - - pub(crate) fn paren(expr: Expr<'a>) -> Self { - Expr::Paren(Box::new(expr)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TypedExpr<'a> { - pub expr: Expr<'a>, - pub typ: Option<Expr<'a>>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FuncParam<'a> { - pub name: Ident<'a>, - pub typ: Expr<'a>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Literal<'a> { - Unit, - Boolean(bool), - Integer(u64), - String(Vec<StringPiece<'a>>), - Tuple(Vec<Expr<'a>>), - Array(Vec<Expr<'a>>), - Map(Vec<MapEntry<'a>>), -} - -impl<'a> Literal<'a> { - pub(crate) fn number(s: &'a str) -> Result<Self, &'static str> { - let (radix, rest) = if let Some(rest) = s.strip_prefix("0x") { - (16, rest) - } else if let Some(rest) = s.strip_prefix("0o") { - (8, rest) - } else if let Some(rest) = s.strip_prefix("0b") { - (2, rest) - } else { - (10, s) - }; - let digits = rest.replace('_', ""); - let value = u64::from_str_radix(&digits, radix).or(Err("number"))?; - Ok(Literal::Integer(value)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum StringPiece<'a> { - Chars(&'a str), - Escape(char), - Interp(Expr<'a>), -} - -impl<'a> TryFrom<&token::StringPiece<'a>> for StringPiece<'a> { - type Error = &'static str; - - fn try_from(value: &token::StringPiece<'a>) -> Result<Self, Self::Error> { - use crate::recipe; - - Ok(match value { - token::StringPiece::Chars(chars) => StringPiece::Chars(chars), - token::StringPiece::Escape(c) => StringPiece::Escape(*c), - token::StringPiece::Interp(tokens) => StringPiece::Interp( - recipe::expr(tokens).or(Err("Invalid expression in string interpolation"))?, - ), - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MapEntry<'a> { - pub key: &'a str, - pub value: Expr<'a>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OpUnary { - Not, - Neg, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OpBinary { - Add, - Sub, - Mul, - Div, - Rem, - And, - Or, - Eq, - Lt, - Le, - Ne, - Ge, - Gt, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Path<'a> { - pub components: Vec<Ident<'a>>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Ident<'a> { - pub name: &'a str, -} diff --git a/crates/rebel-parse/src/ast/expr.rs b/crates/rebel-parse/src/ast/expr.rs new file mode 100644 index 0000000..a35a9af --- /dev/null +++ b/crates/rebel-parse/src/ast/expr.rs @@ -0,0 +1,333 @@ +use std::borrow::Cow; + +use super::{Block, DestrPat, Ident, Path, PathRoot, ValidationError}; +use crate::token; +use derive_into_owned::{Borrowed, IntoOwned}; +use rustc_hash::FxHashSet; + +pub use token::StrKind; + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub enum Expr<'a> { + Binary { + left: Box<Expr<'a>>, + op: OpBinary, + right: Box<Expr<'a>>, + }, + Unary { + op: OpUnary, + expr: Box<Expr<'a>>, + }, + Apply { + expr: Box<Expr<'a>>, + params: Vec<Expr<'a>>, + }, + Method { + expr: Box<Expr<'a>>, + method: Ident<'a>, + params: Vec<Expr<'a>>, + }, + Index { + base: Box<Expr<'a>>, + index: Box<Expr<'a>>, + }, + Field { + base: Box<Expr<'a>>, + field: Ident<'a>, + }, + Block(Block<'a>), + IfElse { + if_blocks: Vec<(Expr<'a>, Block<'a>)>, + else_block: Option<Box<Block<'a>>>, + }, + Paren(Box<Expr<'a>>), + Path(Path<'a>), + Literal(Literal<'a>), +} + +impl<'a> Expr<'a> { + pub(crate) fn binary(left: Expr<'a>, op: OpBinary, right: Expr<'a>) -> Self { + Expr::Binary { + left: Box::new(left), + op, + right: Box::new(right), + } + } + + pub(crate) fn unary(op: OpUnary, expr: Expr<'a>) -> Self { + Expr::Unary { + op, + expr: Box::new(expr), + } + } + + pub(crate) fn apply(expr: Expr<'a>, params: Vec<Expr<'a>>) -> Self { + Expr::Apply { + expr: Box::new(expr), + params, + } + } + + pub(crate) fn method(expr: Expr<'a>, method: Ident<'a>, params: Vec<Expr<'a>>) -> Self { + Expr::Method { + expr: Box::new(expr), + method, + params, + } + } + + pub(crate) fn index(base: Expr<'a>, index: Expr<'a>) -> Self { + Expr::Index { + base: Box::new(base), + index: Box::new(index), + } + } + + pub(crate) fn field(base: Expr<'a>, field: Ident<'a>) -> Self { + Expr::Field { + base: Box::new(base), + field, + } + } + + pub(crate) fn paren(expr: Expr<'a>) -> Self { + Expr::Paren(Box::new(expr)) + } + + pub fn validate(&self) -> Result<(), ValidationError> { + match self { + Expr::Binary { left, op, right } => { + left.validate()?; + right.validate()?; + + if op.is_comparision() + && (left.is_binary_comparison() || right.is_binary_comparison()) + { + return Err(ValidationError::NeedsParens); + } + Ok(()) + } + Expr::Unary { op: _, expr } => expr.validate(), + Expr::Apply { expr, params } => { + for param in params { + param.validate()?; + } + expr.validate() + } + Expr::Method { + expr, + method: _, + params, + } => { + for param in params { + param.validate()?; + } + expr.validate() + } + Expr::Index { base, index } => { + index.validate()?; + base.validate() + } + Expr::Field { base, field: _ } => base.validate(), + Expr::Block(block) => { + for stmt in &block.0 { + stmt.validate()?; + } + Ok(()) + } + Expr::IfElse { + if_blocks, + else_block, + } => { + for (cond, block) in if_blocks { + cond.validate()?; + block.validate()?; + } + if let Some(block) = else_block { + block.validate()?; + } + Ok(()) + } + Expr::Paren(expr) => expr.validate(), + Expr::Path(_) => Ok(()), + Expr::Literal(lit) => lit.validate(), + } + } + + fn is_binary_comparison(&self) -> bool { + let Expr::Binary { + left: _, + op, + right: _, + } = self + else { + return false; + }; + + op.is_comparision() + } +} + +impl<'a> From<DestrPat<'a>> for Expr<'a> { + fn from(value: DestrPat<'a>) -> Self { + match value { + DestrPat::Index { base, index } => Expr::Index { + base: Box::new((*base).into()), + index: index.clone(), + }, + DestrPat::Field { base, field } => Expr::Field { + base: Box::new((*base).into()), + field: field.clone(), + }, + DestrPat::Paren(pat) => Expr::Paren(Box::new((*pat).into())), + DestrPat::Path(path) => Expr::Path(path.clone()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub enum Literal<'a> { + Unit, + None, + Bool(bool), + Int(i64), + Str { + pieces: Vec<StrPiece<'a>>, + kind: StrKind, + }, + Tuple(Vec<Expr<'a>>), + Array(Vec<Expr<'a>>), + Map(Vec<MapEntry<'a>>), + Struct(Vec<StructField<'a>>), +} + +impl<'a> Literal<'a> { + fn validate(&self) -> Result<(), ValidationError> { + match self { + Literal::Unit => Ok(()), + Literal::None => Ok(()), + Literal::Bool(_) => Ok(()), + Literal::Int(_) => Ok(()), + Literal::Str { pieces, kind: _ } => { + for piece in pieces { + match piece { + StrPiece::Chars(_) => {} + StrPiece::Escape(_) => {} + StrPiece::Interp(expr) => expr.validate()?, + } + } + Ok(()) + } + Literal::Tuple(elems) => { + for elem in elems { + elem.validate()?; + } + Ok(()) + } + Literal::Array(elems) => { + for elem in elems { + elem.validate()?; + } + Ok(()) + } + Literal::Map(entries) => { + for MapEntry { key, value } in entries { + key.validate()?; + value.validate()?; + } + Ok(()) + } + Literal::Struct(entries) => { + let mut fields = FxHashSet::default(); + for StructField { name, value } in entries { + if !fields.insert(name) { + return Err(ValidationError::DuplicateKey); + } + value.validate()?; + } + Ok(()) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, IntoOwned, Borrowed)] +pub enum StrPiece<'a> { + Chars(Cow<'a, str>), + Escape(char), + Interp(Expr<'a>), +} + +impl<'a> TryFrom<&token::StrPiece<'a>> for StrPiece<'a> { + type Error = &'static str; + + fn try_from(value: &token::StrPiece<'a>) -> Result<Self, Self::Error> { + use crate::recipe; + + Ok(match value { + token::StrPiece::Chars(chars) => StrPiece::Chars(Cow::Borrowed(chars)), + token::StrPiece::Escape(c) => StrPiece::Escape(*c), + token::StrPiece::Interp(tokens) => StrPiece::Interp( + recipe::expr(tokens).or(Err("Invalid expression in string interpolation"))?, + ), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub struct MapEntry<'a> { + pub key: Expr<'a>, + pub value: Expr<'a>, +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub struct StructField<'a> { + pub name: Cow<'a, str>, + pub value: Expr<'a>, +} + +impl<'a> StructField<'a> { + pub(crate) fn new(field: Ident<'a>, value: Option<Expr<'a>>) -> Self { + let value = value.unwrap_or_else(|| { + Expr::Path(Path { + root: PathRoot::Relative, + components: vec![field.clone()], + }) + }); + StructField { + name: field.name, + value, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpUnary { + Not, + Neg, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpBinary { + Add, + Sub, + Mul, + Div, + Rem, + And, + Or, + Eq, + Lt, + Le, + Ne, + Ge, + Gt, +} + +impl OpBinary { + fn is_comparision(self) -> bool { + use OpBinary::*; + + matches!(self, Eq | Lt | Le | Ne | Ge | Gt) + } +} diff --git a/crates/rebel-parse/src/ast/mod.rs b/crates/rebel-parse/src/ast/mod.rs new file mode 100644 index 0000000..0cdc808 --- /dev/null +++ b/crates/rebel-parse/src/ast/mod.rs @@ -0,0 +1,187 @@ +use std::borrow::Cow; + +use derive_into_owned::{Borrowed, IntoOwned}; +use rustc_hash::FxHashSet; + +pub mod expr; +pub mod pat; +pub mod typ; + +use expr::{Expr, StructField}; +use pat::{DestrPat, Pat}; +use typ::Type; + +pub type Recipe<'a> = Vec<RecipeStmt<'a>>; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RecipeStmt<'a> { + BlockStmt(BlockStmt<'a>), + Fetch { + name: Ident<'a>, + entries: Vec<StructField<'a>>, + }, + Task { + name: Ident<'a>, + params: Vec<FuncParam<'a>>, + block: Block<'a>, + }, +} + +impl<'a> RecipeStmt<'a> { + pub fn validate(&self) -> Result<(), ValidationError> { + match self { + RecipeStmt::BlockStmt(stmt) => stmt.validate(), + RecipeStmt::Fetch { name: _, entries } => { + let mut fields = FxHashSet::default(); + for StructField { name, value } in entries { + if !fields.insert(name) { + return Err(ValidationError::DuplicateKey); + } + value.validate()?; + } + Ok(()) + } + RecipeStmt::Task { + name: _, + params: _, + block, + } => { + // TODO: Validate params? + block.validate() + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub struct Block<'a>(pub Vec<BlockStmt<'a>>); + +impl<'a> Block<'a> { + pub fn validate(&self) -> Result<(), ValidationError> { + for stmt in &self.0 { + stmt.validate()?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub enum BlockStmt<'a> { + Let { + dest: Box<TypedPat<'a>>, + expr: Option<Box<Expr<'a>>>, + }, + Assign { + dest: Box<DestrPat<'a>>, + expr: Box<Expr<'a>>, + }, + Fn { + ident: Ident<'a>, + params: Vec<FuncParam<'a>>, + ret: Option<Box<Type<'a>>>, + block: Block<'a>, + }, + Expr { + expr: Box<Expr<'a>>, + }, + Empty, +} + +impl<'a> BlockStmt<'a> { + pub(crate) fn let_assign(dest: TypedPat<'a>, expr: Option<Expr<'a>>) -> Self { + BlockStmt::Let { + dest: Box::new(dest), + expr: expr.map(Box::new), + } + } + + pub(crate) fn assign( + dest: DestrPat<'a>, + op: Option<expr::OpBinary>, + swapped: bool, + expr: Expr<'a>, + ) -> Self { + let expr = match op { + Some(op) => { + let dest_expr = Expr::from(dest.clone()); + if swapped { + Expr::binary(expr, op, dest_expr) + } else { + Expr::binary(dest_expr, op, expr) + } + } + None => expr, + }; + BlockStmt::Assign { + dest: Box::new(dest), + expr: Box::new(expr), + } + } + + pub fn validate(&self) -> Result<(), ValidationError> { + match self { + BlockStmt::Let { dest, expr } => { + let TypedPat { pat, typ: _ } = dest.as_ref(); + pat.validate()?; + if let Some(expr) = expr { + expr.validate()?; + } + Ok(()) + } + BlockStmt::Assign { dest, expr } => { + dest.validate()?; + expr.validate()?; + Ok(()) + } + BlockStmt::Fn { + ident: _, + params: _, + ret: _, + block, + } => { + // TODO: Validate params? + block.validate() + } + BlockStmt::Expr { expr } => expr.validate(), + BlockStmt::Empty => Ok(()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub struct TypedPat<'a> { + pub pat: Pat<'a>, + pub typ: Option<Type<'a>>, +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub struct FuncParam<'a> { + pub name: Ident<'a>, + pub typ: Type<'a>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PathRoot { + Absolute, + Relative, + Recipe, + Task, +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub struct Path<'a> { + pub root: PathRoot, + pub components: Vec<Ident<'a>>, +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub struct Ident<'a> { + pub name: Cow<'a, str>, +} + +#[derive(Debug, Clone, Copy)] +pub enum ValidationError { + DuplicateKey, + NeedsParens, + InvalidLet, +} diff --git a/crates/rebel-parse/src/ast/pat.rs b/crates/rebel-parse/src/ast/pat.rs new file mode 100644 index 0000000..c85f625 --- /dev/null +++ b/crates/rebel-parse/src/ast/pat.rs @@ -0,0 +1,57 @@ +use super::*; + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub enum Pat<'a> { + Paren(Box<Pat<'a>>), + Ident(Ident<'a>), +} + +impl<'a> Pat<'a> { + pub fn validate(&self) -> Result<(), ValidationError> { + match self { + Pat::Paren(pat) => pat.validate(), + Pat::Ident(_) => Ok(()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub enum DestrPat<'a> { + Index { + base: Box<DestrPat<'a>>, + index: Box<Expr<'a>>, + }, + Field { + base: Box<DestrPat<'a>>, + field: Ident<'a>, + }, + Paren(Box<DestrPat<'a>>), + Path(Path<'a>), +} + +impl<'a> DestrPat<'a> { + pub fn validate(&self) -> Result<(), ValidationError> { + match self { + DestrPat::Index { base, index } => { + base.validate()?; + index.validate()?; + Ok(()) + } + DestrPat::Field { base, field: _ } => base.validate(), + DestrPat::Paren(pat) => pat.validate(), + DestrPat::Path(_) => Ok(()), + } + } +} + +impl<'a> From<Pat<'a>> for DestrPat<'a> { + fn from(value: Pat<'a>) -> Self { + match value { + Pat::Paren(pat) => DestrPat::Paren(Box::new((*pat).into())), + Pat::Ident(ident) => DestrPat::Path(Path { + root: PathRoot::Relative, + components: vec![ident], + }), + } + } +} diff --git a/crates/rebel-parse/src/ast/typ.rs b/crates/rebel-parse/src/ast/typ.rs new file mode 100644 index 0000000..54ab1b9 --- /dev/null +++ b/crates/rebel-parse/src/ast/typ.rs @@ -0,0 +1,28 @@ +use std::borrow::Cow; + +use derive_into_owned::{Borrowed, IntoOwned}; + +use super::Path; + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub enum Type<'a> { + Paren(Box<Type<'a>>), + Option(Box<Type<'a>>), + Path(Path<'a>), + Literal(Literal<'a>), +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub enum Literal<'a> { + Unit, + Tuple(Vec<Type<'a>>), + Array(Box<Type<'a>>), + Map(Box<Type<'a>>, Box<Type<'a>>), + Struct(Vec<StructField<'a>>), +} + +#[derive(Debug, Clone, PartialEq, Eq, IntoOwned, Borrowed)] +pub struct StructField<'a> { + pub name: Cow<'a, str>, + pub typ: Type<'a>, +} diff --git a/crates/rebel-parse/src/grammar/recipe.rs b/crates/rebel-parse/src/grammar/recipe.rs index 49b1689..81b47c9 100644 --- a/crates/rebel-parse/src/grammar/recipe.rs +++ b/crates/rebel-parse/src/grammar/recipe.rs @@ -1,46 +1,126 @@ -use crate::ast::{self, Expr}; -use crate::token::*; +use std::borrow::Cow; + +use crate::{ + ast::{ + self, + expr::{self, Expr}, + pat::{DestrPat, Pat}, + typ::{self, Type}, + }, + token::*, +}; pub use rules::*; peg::parser! { pub grammar rules<'a>() for TokenStream<'a> { - use ast::OpBinary::*; - use ast::OpUnary::*; + use expr::OpBinary::*; + use expr::OpUnary::*; pub rule recipe() -> ast::Recipe<'a> = recipe:recipe_stmt()* { recipe } pub rule recipe_stmt() -> ast::RecipeStmt<'a> - = keyword_fetch() name:ident() p('{') body:body() p('}') { - ast::RecipeStmt::Fetch { name, body: Vec::new() } + = [Token::Keyword(Keyword::Fetch)] name:ident() p('{') entries:delimited(<struct_field()>, <p(',')>) p('}') { + ast::RecipeStmt::Fetch { name, entries } } - / keyword_task() name:ident() p('(') params:func_params() p(')') - p('{') body:body() p('}') { - ast::RecipeStmt::Task { name, params, body } + / [Token::Keyword(Keyword::Task)] name:ident() p('(') params:func_params() p(')') + p('{') block:block() p('}') { + ast::RecipeStmt::Task { name, params, block } } - / stmt:body_stmt() { - ast::RecipeStmt::BodyStmt(stmt) + / stmt:block_stmt() p(';') { + ast::RecipeStmt::BlockStmt(stmt) } - pub rule body() -> ast::Body<'a> - = body:body_stmt()* { body } + pub rule block() -> ast::Block<'a> + = block:block_stmt() ++ p(';') { ast::Block(block) } - pub rule body_stmt() -> ast::BodyStmt<'a> - = left:typed_expr() op:assign_op() right:expr() p(';') { - ast::BodyStmt::assign(left, op, right) + pub rule block_stmt() -> ast::BlockStmt<'a> + = [Token::Keyword(Keyword::Let)] dest:typed_pat() p('=') expr:expr() { + ast::BlockStmt::let_assign(dest, Some(expr)) + } + / [Token::Keyword(Keyword::Let)] dest:typed_pat() { + ast::BlockStmt::let_assign(dest, None) + } + / [Token::Keyword(Keyword::Fn)] ident:ident() p('(') params:func_params() p(')') + ret:tagged(<p2('-', '>')>, <typ()>)? p('{') block:block() p('}') + { + ast::BlockStmt::Fn { + ident, + params, + ret: ret.map(Box::new), + block, + } + } + / dest:destr_pat() op:assign_op() expr:expr() { + ast::BlockStmt::assign(dest, op, false, expr) + } + / dest:destr_pat() p2('=', '+') expr:expr() { + ast::BlockStmt::assign(dest, Some(Add), true, expr) } + / expr:expr() { + ast::BlockStmt::Expr { expr: Box::new(expr) } + } + / { ast::BlockStmt::Empty } - rule assign_op() -> Option<ast::OpBinary> - = p2('+', '=') { Some(Add) } + rule assign_op() -> Option<expr::OpBinary> + = p('=') { None } + / p2('+', '=') { Some(Add) } / p2('-', '=') { Some(Sub) } / p2('*', '=') { Some(Mul) } / p2('/', '=') { Some(Div) } / p2('%', '=') { Some(Rem) } - / p('=') { None } - rule typed_expr() -> ast::TypedExpr<'a> - = expr:expr() typ:tagged(<p(':')>, <expr()>)? { ast::TypedExpr { expr, typ } } + rule typed_pat() -> ast::TypedPat<'a> + = pat:pat() typ:tagged(<p(':')>, <typ()>)? { ast::TypedPat { pat, typ } } + + pub rule typ() -> Type<'a> = precedence! { + t:@ p('?') { Type::Option(Box::new(t)) } + -- + t:typ_atom() { t } + } + + rule typ_atom() -> Type<'a> + = p('(') t:typ() p(')') { Type::Paren(Box::new(t)) } + / lit:typ_literal() { Type::Literal(lit) } + / path:path() { Type::Path(path) } + + rule typ_literal() -> typ::Literal<'a> + = p('(') p(')') { typ::Literal::Unit } + / p('(') elements:(typ() ++ p(',')) p(',')? p(')') { + typ::Literal::Tuple(elements) + } + / p('[') typ:typ() p(']') { + typ::Literal::Array(Box::new(typ)) + } + / [Token::Keyword(Keyword::Map)] p('{') key:typ() p2('=', '>') value:typ() p('}') { + typ::Literal::Map(Box::new(key), Box::new(value)) + } + / p('{') entries:delimited(<struct_field_typ()>, <p(',')>) p('}') { + typ::Literal::Struct(entries) + } + + pub rule pat() -> ast::pat::Pat<'a> + = p('(') pat:pat() p(')') { Pat::Paren(Box::new(pat)) } + / ident:ident() { Pat::Ident(ident) } + + pub rule destr_pat() -> DestrPat<'a> = precedence! { + base:@ p('[') index:expr() p(']') { + DestrPat::Index { base: Box::new(base), index: Box::new(index) } + } + -- + base:@ p('.') field:field() { + DestrPat::Field { base: Box::new(base), field } + } + -- + p('(') pat:destr_pat() p(')') { DestrPat::Paren(Box::new(pat)) } + path:path() { DestrPat::Path(path) } + } + + rule struct_field_typ() -> typ::StructField<'a> + = field:field() p(':') typ:typ() { + typ::StructField { name: field.name, typ } + } pub rule expr() -> Expr<'a> = precedence! { left:(@) p2('|', '|') right:@ { Expr::binary(left, Or, right) } @@ -67,63 +147,112 @@ peg::parser! { expr:@ p('(') params:call_params() p(')') { Expr::apply(expr, params) } - expr:@ p('[') index:expr() p(']') { Expr::index(expr, index) } + base:@ p('[') index:expr() p(']') { Expr::index(base, index) } -- expr:@ p('.') method:field() p('(') params:call_params() p(')') { Expr::method(expr, method, params) } - expr:@ p('.') field:field() { Expr::field(expr, field) } + base:@ p('.') field:field() { Expr::field(base, field) } -- - p('(') e:expr() p(')') { Expr::paren(e) } e:atom() { e } } rule atom() -> Expr<'a> - = lit:literal() { Expr::Literal(lit) } + = p('(') e:expr() p(')') { Expr::paren(e) } + / [Token::Keyword(Keyword::If)] + if_blocks:(cond_block() ++ ([Token::Keyword(Keyword::Else)] [Token::Keyword(Keyword::If)])) + else_block:([Token::Keyword(Keyword::Else)] p('{') block:block() p('}') { Box::new(block) })? + { + Expr::IfElse { if_blocks, else_block } + } + / lit:literal() { Expr::Literal(lit) } + / p('{') block:block() p('}') { Expr::Block(block) } / path:path() { Expr::Path(path) } - rule call_params() -> Vec<ast::Expr<'a>> + rule cond_block() -> (Expr<'a>, ast::Block<'a>) + = cond:expr() p('{') block:block() p('}') { (cond, block) } + + rule call_params() -> Vec<expr::Expr<'a>> = args:delimited(<expr()>, <p(',')>) { args } rule func_params() -> Vec<ast::FuncParam<'a>> = params:delimited(<func_param()>, <p(',')>) { params } rule func_param() -> ast::FuncParam<'a> - = name:ident() p(':') typ:expr() { ast::FuncParam { name, typ } } + = name:ident() p(':') typ:typ() { ast::FuncParam { name, typ } } - rule literal() -> ast::Literal<'a> - = keyword_true() { ast::Literal::Boolean(true) } - / keyword_false() { ast::Literal::Boolean(false) } - / [Token::Number(content)] { ? - ast::Literal::number(content) - } - / [Token::String(String { pieces, .. })] { ? - let ast_pieces = pieces.iter().map(|piece| piece.try_into()).collect::<Result<_, _>>()?; - Ok(ast::Literal::String(ast_pieces)) + rule literal() -> expr::Literal<'a> + = [Token::Keyword(Keyword::True)] { expr::Literal::Bool(true) } + / [Token::Keyword(Keyword::False)] { expr::Literal::Bool(false) } + / [Token::Keyword(Keyword::None)] { expr::Literal::None } + / n:number() { expr::Literal::Int(n) } + / [Token::Str(Str { pieces, kind })] { ? + let pieces = pieces + .iter() + .map(|piece| piece.try_into()) + .collect::<Result<_, _>>()?; + Ok(expr::Literal::Str{ pieces, kind: *kind }) } - / p('(') p(')') { ast::Literal::Unit } - / p('(') elements:(expr() ** p(',')) p(',')? p(')') { - ast::Literal::Tuple(elements) + / p('(') p(')') { expr::Literal::Unit } + / p('(') elements:(expr() ++ p(',')) p(',')? p(')') { + expr::Literal::Tuple(elements) } / p('[') elements:delimited(<expr()>, <p(',')>) p(']') { - ast::Literal::Array(elements) + expr::Literal::Array(elements) } - / p('{') entries:delimited(<map_entry()>, <p(',')>) p('}') { - ast::Literal::Map(entries) + / [Token::Keyword(Keyword::Map)] p('{') entries:delimited(<map_entry()>, <p(',')>) p('}') { + expr::Literal::Map(entries) + } + / p('{') entries:delimited(<struct_field()>, <p(',')>) p('}') { + expr::Literal::Struct(entries) + } + + rule map_entry() -> expr::MapEntry<'a> + = key:expr() p2('=', '>') value:expr() { + expr::MapEntry { key, value } } - rule map_entry() -> ast::MapEntry<'a> - = key:field() p('=') value:expr() { - ast::MapEntry { key: key.name, value } + rule struct_field() -> expr::StructField<'a> + = field:field() value:tagged(<p(':')>, <expr()>)? { + expr::StructField::new(field, value) } rule path() -> ast::Path<'a> - = components:ident() ++ p2(':', ':') { ast::Path { components } } + = components:(ident() ++ p2(':', ':')) { + ast::Path { root: ast::PathRoot::Relative, components } + } + / components:(p2(':', ':') ident:ident() { ident })+ { + ast::Path { root: ast::PathRoot::Absolute, components } + } + / [Token::Keyword(Keyword::Recipe)] components:(p2(':', ':') ident:ident() { ident })* { + ast::Path { root: ast::PathRoot::Recipe, components } + } + / [Token::Keyword(Keyword::Task)] components:(p2(':', ':') ident:ident() { ident })* { + ast::Path { root: ast::PathRoot::Task, components } + } rule field() -> ast::Ident<'a> = ident() / [Token::Number(content)] { - ast::Ident { name: content } + ast::Ident { name: Cow::Borrowed(content) } + } + + rule number() -> i64 + = neg:p('-')? [Token::Number(s)] { ? + let (radix, rest) = if let Some(rest) = s.strip_prefix("0x") { + (16, rest) + } else if let Some(rest) = s.strip_prefix("0o") { + (8, rest) + } else if let Some(rest) = s.strip_prefix("0b") { + (2, rest) + } else { + (10, *s) + }; + let mut digits = rest.replace('_', ""); + if neg.is_some() { + digits = format!("-{digits}"); + } + i64::from_str_radix(&digits, radix).or(Err("number")) } rule p_(ch: char) @@ -132,29 +261,11 @@ peg::parser! { rule p(ch: char) -> () = [Token::Punct(Punct(c, _)) if *c == ch] {} - rule p2(ch1: char, ch2: char) + rule p2(ch1: char, ch2: char) -> () = p_(ch1) p(ch2) rule ident() -> ast::Ident<'a> - = !keyword() [Token::Ident(name)] { ast::Ident { name } } - - rule keyword() - = keyword_true() - / keyword_false() - / keyword_fetch() - / keyword_task() - - rule keyword_true() - = const_ident("true") - rule keyword_false() - = const_ident("false") - rule keyword_fetch() - = const_ident("fetch") - rule keyword_task() - = const_ident("task") - - rule const_ident(keyword: &str) - = [Token::Ident(name) if *name == keyword] + = [Token::Ident(name)] { ast::Ident { name: Cow::Borrowed(name) } } rule delimited<T>(expr: rule<T>, delim: rule<()>) -> Vec<T> = values:(expr() ++ delim()) delim()? { values } diff --git a/crates/rebel-parse/src/grammar/tokenize.rs b/crates/rebel-parse/src/grammar/tokenize.rs index 8ca9d59..eb8a900 100644 --- a/crates/rebel-parse/src/grammar/tokenize.rs +++ b/crates/rebel-parse/src/grammar/tokenize.rs @@ -2,6 +2,23 @@ use crate::token::*; pub use rules::*; +static KEYWORDS: phf::Map<&'static str, Keyword> = phf::phf_map! { + "else" => Keyword::Else, + "false" => Keyword::False, + "fetch" => Keyword::Fetch, + "fn" => Keyword::Fn, + "for" => Keyword::For, + "if" => Keyword::If, + "let" => Keyword::Let, + "map" => Keyword::Map, + "mut" => Keyword::Mut, + "none" => Keyword::None, + "recipe" => Keyword::Recipe, + "set" => Keyword::Set, + "task" => Keyword::Task, + "true" => Keyword::True, +}; + peg::parser! { pub grammar rules() for str { pub rule token_stream() -> TokenStream<'input> @@ -9,21 +26,32 @@ peg::parser! { pub rule token() -> Token<'input> = number:number() { Token::Number(number) } - / string:string() { Token::String(string) } - / ident:ident() { Token::Ident(ident) } + / string:string() { Token::Str(string) } + / token:ident_or_keyword() { token } / punct:punct() { Token::Punct(punct) } - rule ident() -> &'input str - = $( + rule ident_or_keyword() -> Token<'input> + = s:$( ['a'..='z' | 'A' ..='Z' | '_' ] ['a'..='z' | 'A' ..='Z' | '_' | '0'..='9']* - ) + ) { + if let Some(kw) = KEYWORDS.get(s) { + Token::Keyword(*kw) + } else { + Token::Ident(s) + } + } rule punct() -> Punct = ch:punct_char() spacing:spacing() { Punct(ch, spacing) } rule punct_char() -> char - = !number() !string() !ident() !__ ch:[_] { ch } + = !comment_start() ch:[ + | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' + | '*' | '-' | '=' | '+' | '|' | ';' | ':' | ',' + | '<' | '.' | '>' | '/' | '\'' | '?' | '(' | ')' + | '[' | ']' | '{' | '}' + ] { ch } rule spacing() -> Spacing = &punct_char() { Spacing::Joint } @@ -32,29 +60,29 @@ peg::parser! { rule number() -> &'input str = $(['0'..='9'] ['0'..='9' | 'a'..='z' | 'A'..='Z' | '_']*) - rule string() -> String<'input> + rule string() -> Str<'input> = "\"" pieces:string_piece()* "\"" { - String { + Str { pieces, - kind: StringKind::String, + kind: StrKind::Regular, } } / "r\"" chars:$([^'"']*) "\"" { - String { - pieces: vec![StringPiece::Chars(chars)], - kind: StringKind::RawString, + Str { + pieces: vec![StrPiece::Chars(chars)], + kind: StrKind::Raw, } } / "```" newline() pieces:script_string_piece()* "```" { - String { + Str { pieces, - kind: StringKind::ScriptString, + kind: StrKind::Script, } } - rule string_piece() -> StringPiece<'input> - = chars:$((!"{{" [^'"' | '\\'])+) { StringPiece::Chars(chars) } - / "\\" escape:string_escape() { StringPiece::Escape(escape) } + rule string_piece() -> StrPiece<'input> + = chars:$((!"{{" [^'"' | '\\'])+) { StrPiece::Chars(chars) } + / "\\" escape:string_escape() { StrPiece::Escape(escape) } / string_interp() rule string_escape() -> char @@ -63,6 +91,7 @@ peg::parser! { / "t" { '\t' } / "\\" { '\\' } / "\"" { '"' } + / "{" { '{' } / "0" { '\0' } / "x" digits:$(['0'..='7'] hex_digit()) { u8::from_str_radix(digits, 16).unwrap().into() @@ -71,13 +100,13 @@ peg::parser! { u32::from_str_radix(digits, 16).unwrap().try_into().or(Err("Invalid unicode escape")) } - rule script_string_piece() -> StringPiece<'input> - = chars:$((!"{{" !"```" [_])+) { StringPiece::Chars(chars) } + rule script_string_piece() -> StrPiece<'input> + = chars:$((!"{{" !"```" [_])+) { StrPiece::Chars(chars) } / string_interp() - rule string_interp() -> StringPiece<'input> - = "{{" tokens:subtoken()* "}}" { - StringPiece::Interp(TokenStream(tokens)) + rule string_interp() -> StrPiece<'input> + = "{{" _ tokens:(subtoken() ++ _) _ "}}" { + StrPiece::Interp(TokenStream(tokens)) } rule subtoken() -> Token<'input> @@ -94,6 +123,10 @@ peg::parser! { rule _ = quiet!{__?} + rule comment_start() + = "//" + / "/*" + rule comment() = "//" (!newline() [_])* (newline() / ![_]) / "/*" (!"*/" [_])* "*/" diff --git a/crates/rebel-parse/src/token.rs b/crates/rebel-parse/src/token.rs index 2f2f849..444b5a8 100644 --- a/crates/rebel-parse/src/token.rs +++ b/crates/rebel-parse/src/token.rs @@ -1,12 +1,31 @@ #[derive(Clone, Debug, PartialEq, Eq)] pub enum Token<'a> { + Keyword(Keyword), Ident(&'a str), Punct(Punct), - String(String<'a>), + Str(Str<'a>), Number(&'a str), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Keyword { + Else, + False, + Fetch, + Fn, + For, + If, + Let, + Map, + Mut, + None, + Recipe, + Set, + Task, + True, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Punct(pub char, pub Spacing); #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -16,23 +35,23 @@ pub enum Spacing { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct String<'a> { - pub pieces: Vec<StringPiece<'a>>, - pub kind: StringKind, +pub struct Str<'a> { + pub pieces: Vec<StrPiece<'a>>, + pub kind: StrKind, } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum StringPiece<'a> { +pub enum StrPiece<'a> { Chars(&'a str), Escape(char), Interp(TokenStream<'a>), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum StringKind { - String, - RawString, - ScriptString, +pub enum StrKind { + Regular, + Raw, + Script, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/examples/recipes/gmp/build.recipe b/examples/recipes/gmp/build.recipe index 44e87d6..19a25f6 100644 --- a/examples/recipes/gmp/build.recipe +++ b/examples/recipes/gmp/build.recipe @@ -7,63 +7,61 @@ // build_depend: (task: TaskID) -> TaskDep // host_depend: (task: TaskID) -> TaskDep -version = "6.3.0"; -sourcedir = "{{workdir}}/{{name}}-{{version}}"; -builddir = "{{workdir}}/{{name}}-build"; +let version = "6.3.0"; +let sourcedir = "{{workdir}}/{{name}}-{{version}}"; +let builddir = "{{workdir}}/{{name}}-build"; fetch source { - url = ["https://invalid/{{name}}-{{version}}.tar.xz"]; + url: ["https://invalid/{{name}}-{{version}}.tar.xz"], // TODO: Move to lockfile - sha256 = "a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898"; + sha256: "a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898", } task unpack() { - depends = [source]; + task.depends = [source]; - run = ``` + ``` tar xf {{source.path}} - ```; + ``` } task configure(host: Platform) { - parent = unpack(); - depends = [ + task.parent = unpack(); + task.depends = [ build_depend(toolchain::build_depends), host_depend(toolchain::depends), ]; - run = ``` + ``` mkdir {{builddir}} cd {{builddir}} {{sourcedir}}/configure \ --build={{build.gnu_triplet}} \ --host={{host.gnu_triplet}} \ --prefix={{host.prefix}} - ```; + ``` } task compile(host: Platform) { - parent = configure(host); + task.parent = configure(host); - run = ``` + ``` cd {{builddir}} make - ```; + ``` } task install(host: Platform) { - parent = compile(host); + task.parent = compile(host); - output = { - default = { - runtime_depends = [host_depend(toolchain::depends)], - }, + task.output["default"] = { + runtime_depends: [host_depend(toolchain::depends)], }; - run = ``` + ``` cd {{builddir}} make DESTDIR={{destdir}} install rm {{destdir}}{{host.prefix}}/lib/*.a rm {{destdir}}{{host.prefix}}/lib/*.la - ```; + ``` } |