commit 15e5ccb06483762cf4d1c531304e60aadbf67b5f Author: Brandon Lucas Date: Fri Feb 13 02:57:01 2026 -0500 init lux diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d787b70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/result diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c5089bd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,670 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lux" +version = "0.1.0" +dependencies = [ + "rustyline", + "tempfile", + "thiserror", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustyline" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.52.0", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e8bdecf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lux" +version = "0.1.0" +edition = "2021" +description = "A functional programming language with first-class effects, schema evolution, and behavioral types" +license = "MIT" + +[dependencies] +rustyline = "14" +thiserror = "1" + +[dev-dependencies] +tempfile = "3" + +[profile.release] +lto = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..59c5dcc --- /dev/null +++ b/README.md @@ -0,0 +1,213 @@ +# Lux + +A functional programming language with first-class effects, schema evolution, and behavioral types. + +## Vision + +Most programming languages treat three critical concerns as afterthoughts: + +1. **Effects** — What can this code do? (Hidden, untraceable, untestable) +2. **Data Evolution** — Types change, data persists. (Manual migrations, runtime failures) +3. **Behavioral Properties** — Is this idempotent? Does it terminate? (Comments and hope) + +Lux makes these first-class language features. The compiler knows what your code does, how your data evolves, and what properties your functions guarantee. + +## Core Principles + +### 1. Effects Are Explicit and Composable + +```lux +fn fetchUser(id: UserId): User with {Database, Http} = + let profile = Http.get("/users/{id}") + let prefs = Database.query(userPrefsQuery(id)) + User.merge(profile, prefs) + +-- Testing: swap real effects for mocks +test "fetchUser returns merged data" = + run fetchUser(testId) with { + Database = mockDb({ testId: testPrefs }), + Http = mockHttp({ "/users/{testId}": testProfile }) + } + |> Assert.eq(expectedUser) +``` + +No hidden side effects. No dependency injection boilerplate. Effects are declared, handlers are swappable, composition just works. + +### 2. Schema Evolution Is Built-In + +```lux +type User @v1 { + name: String, + email: String +} + +type User @v2 { + name: String, + email: String, + age: Option -- optional field: auto-compatible +} + +type User @v3 { + fullName: String, -- renamed: requires migration + email: String, + age: Option, + + from @v2 = { fullName: v2.name, ..v2 } +} +``` + +The compiler tracks compatibility. Breaking changes are compile errors. Migrations are code, not config. + +### 3. Behavioral Types Are First-Class + +```lux +fn retry(action: F): Result + where F: fn() -> T with {Fail}, + where F is idempotent -- enforced! += + match action() { + Ok(v) => Ok(v), + Err(_) => action() -- safe: we know it's idempotent + } + +fn sort(list: List): List + is pure, + is total, + where result.len == list.len, + where result.isSorted +``` + +Properties like `pure`, `total`, `idempotent`, `commutative` are part of the type system. The compiler proves what it can, tests what it can't. + +## Example + +```lux +-- Define an effect +effect Logger { + fn log(level: Level, msg: String): Unit +} + +-- Define a versioned type +type Config @v1 { + host: String, + port: Int +} + +type Config @v2 { + host: String, + port: Int, + timeout: Duration, + + from @v1 = { timeout: Duration.seconds(30), ..v1 } +} + +-- A function with explicit effects and properties +fn loadConfig(path: Path): Config @v2 with {FileSystem, Logger} + is total += + Logger.log(Info, "Loading config from {path}") + let raw = FileSystem.read(path) + Config.parse(raw) + +-- Run with handlers +fn main(): Unit with {Console} = + let config = run loadConfig("./config.json") with { + FileSystem = realFs, + Logger = consoleLogger + } + Console.print("Loaded: {config}") +``` + +## Status + +**Current Phase: Prototype Implementation** + +The interpreter is functional with: +- Core language (functions, closures, pattern matching) +- Effect system (declare effects, use operations, handle with handlers) +- Type checking with effect tracking +- REPL for interactive development + +See: +- [SKILLS.md](./SKILLS.md) — Language specification and implementation roadmap +- [docs/VISION.md](./docs/VISION.md) — Problems Lux solves and development roadmap +- [docs/OVERVIEW.md](./docs/OVERVIEW.md) — Use cases, pros/cons, complexity analysis + +## Design Goals + +| Goal | Approach | +|------|----------| +| **Correctness by default** | Effects, schemas, and behaviors are compiler-checked | +| **Incremental adoption** | Start simple, add properties/versions as needed | +| **Zero-cost abstractions** | Effect handlers inline, versions compile away | +| **Practical, not academic** | Familiar syntax, clear errors, gradual verification | + +## Non-Goals + +- Not a systems language (no manual memory management) +- Not a scripting language (static types required) +- Not a proof assistant (verification is practical, not total) + +## Building + +Requires Rust 1.70+: + +```bash +# Build the interpreter +cargo build --release + +# Run the REPL +cargo run + +# Run a file +cargo run -- examples/hello.lux + +# Run tests +cargo test +``` + +## Examples + +See the `examples/` directory: + +- `hello.lux` — Hello World with effects +- `factorial.lux` — Recursive functions +- `effects.lux` — Custom effects and handlers +- `datatypes.lux` — ADTs and pattern matching +- `functional.lux` — Higher-order functions and pipes + +### Quick REPL Session + +``` +$ cargo run +Lux v0.1.0 +Type :help for help, :quit to exit + +lux> let x = 42 +lux> x * 2 +84 +lux> fn double(n: Int): Int = n * 2 +lux> double(21) +42 +lux> [1, 2, 3] |> List.reverse +[3, 2, 1] +lux> List.map([1, 2, 3], double) +[2, 4, 6] +lux> String.split("a,b,c", ",") +["a", "b", "c"] +lux> Some(42) |> Option.map(double) +Some(84) +lux> :quit +``` + +## Contributing + +This project is in early design. Contributions welcome in: +- Language design discussions (open an issue) +- Syntax bikeshedding +- Semantic formalization +- Compiler implementation (once design stabilizes) + +## License + +MIT diff --git a/SKILLS.md b/SKILLS.md new file mode 100644 index 0000000..a8a1dc2 --- /dev/null +++ b/SKILLS.md @@ -0,0 +1,734 @@ +# Lux Language Skills & Implementation Plan + +## Table of Contents + +1. [Effect System](#1-effect-system) +2. [Schema Evolution](#2-schema-evolution) +3. [Behavioral Types](#3-behavioral-types) +4. [Type System Foundation](#4-type-system-foundation) +5. [Implementation Roadmap](#5-implementation-roadmap) + +--- + +## 1. Effect System + +### Overview + +Effects make side effects explicit, trackable, and testable. Every function declares what it can do. Handlers interpret effects at runtime. + +### Core Concepts + +#### Effect Declarations + +```lux +effect Http { + fn get(url: String): Response + fn post(url: String, body: Bytes): Response +} + +effect Database { + fn query(q: Query): List + fn execute(q: Command): Int +} + +effect Logger { + fn log(level: Level, message: String): Unit +} + +effect Async { + fn await(future: Future): T + fn spawn(action: fn(): T): Future +} +``` + +#### Effect Signatures + +Functions declare their effects after `with`: + +```lux +fn fetchUsers(): List with {Database, Logger} = + Logger.log(Info, "Fetching users") + Database.query(selectAllUsers) + +-- Multiple effects compose naturally +fn syncUsers(source: Url): Int with {Http, Database, Logger} = + let users = Http.get(source) |> parseUsers + Logger.log(Info, "Syncing {users.len} users") + users |> List.map(upsertUser) |> List.sum +``` + +#### Effect Handlers + +Handlers provide implementations: + +```lux +handler realHttp: Http { + fn get(url) = httpClientGet(url) + fn post(url, body) = httpClientPost(url, body) +} + +handler mockHttp(responses: Map): Http { + fn get(url) = responses.get(url).unwrapOr(Response.notFound) + fn post(url, _) = responses.get(url).unwrapOr(Response.notFound) +} + +handler postgresDb(conn: Connection): Database { + fn query(q) = conn.execute(q.toSql) |> parseRows + fn execute(q) = conn.execute(q.toSql) +} +``` + +#### Running Effects + +```lux +fn main(): Unit with {Console} = + let users = run fetchUsers() with { + Database = postgresDb(openConnection()), + Logger = consoleLogger + } + Console.print("Found {users.len} users") +``` + +### Effect Features + +#### Effect Polymorphism + +Write code generic over effects: + +```lux +fn withRetry(action: fn(): T with E, attempts: Int): T with E = + match attempts { + 0 => panic("Retry exhausted"), + n => try action() catch _ => withRetry(action, n - 1) + } +``` + +#### Effect Constraints + +Require specific effects: + +```lux +fn transactional(action: fn(): T with {Database}): T with {Database} = + Database.execute(begin) + let result = try action() catch e => { + Database.execute(rollback) + throw e + } + Database.execute(commit) + result +``` + +#### Effect Inference + +Effects can be inferred within function bodies, but signatures must be explicit: + +```lux +fn helper() with {Logger} = -- explicit signature + let x = compute() -- effect-free, inferred + Logger.log(Debug, "x = {x}") -- Logger effect used + x +``` + +#### Built-in Effects + +```lux +effect Fail { + fn fail(error: Error): T -- early return / exceptions +} + +effect State { + fn get(): S + fn put(s: S): Unit + fn modify(f: fn(S): S): Unit +} + +effect Reader { + fn ask(): R +} + +effect Random { + fn int(range: Range): Int + fn float(): Float + fn shuffle(list: List): List +} + +effect Time { + fn now(): Instant + fn sleep(duration: Duration): Unit +} +``` + +### Effect Semantics + +- **Lexical handling**: Effects are handled at `run` boundaries +- **Order independence**: Multiple effects can be handled in any order +- **Resumable**: Handlers can resume computations (algebraic effect style) +- **Zero-cost goal**: Handlers inline when statically known + +--- + +## 2. Schema Evolution + +### Overview + +Types change over time. Data persists. Lux tracks type versions and ensures compatibility at compile time. + +### Core Concepts + +#### Versioned Types + +```lux +type User @v1 { + name: String, + email: String +} + +type User @v2 { + name: String, + email: String, + createdAt: Timestamp +} +``` + +#### Compatibility Rules + +**Auto-compatible changes** (no migration needed): +- Adding optional fields +- Adding fields with defaults +- Widening numeric types (Int32 -> Int64) +- Adding enum variants (for extensible enums) + +**Breaking changes** (require explicit migration): +- Removing fields +- Renaming fields +- Changing field types +- Removing enum variants + +```lux +type User @v3 { + fullName: String, -- renamed from 'name' + email: String, + createdAt: Timestamp, + + -- Explicit migration required + from @v2 = { + fullName: v2.name, + email: v2.email, + createdAt: v2.createdAt + } +} +``` + +#### Migration Chains + +Migrations compose automatically: + +```lux +-- Reading @v1 data as @v3: +-- 1. @v1 -> @v2 (auto: createdAt gets default) +-- 2. @v2 -> @v3 (explicit: name -> fullName) + +fn loadLegacyUser(data: Bytes): User @v3 = + Codec.decode(data) -- handles any version +``` + +#### Version Constraints + +```lux +-- Accept any version >= @v2 +fn processUser(user: User @v2+): Unit = ... + +-- Accept exactly @v3 +fn processUserV3(user: User @v3): Unit = ... + +-- Return latest version +fn createUser(name: String): User @latest = ... +``` + +### Schema Features + +#### Serialization + +```lux +-- Encode with version tag +let bytes = Codec.encode(user) -- includes version marker + +-- Decode to specific version (migrates if needed) +let user: User @v3 = Codec.decode(bytes) + +-- Decode to any compatible version +let user: User @v2+ = Codec.decode(bytes) +``` + +#### Database Integration + +```lux +table users: User @v3 { + primaryKey: id, + index: [email] +} + +-- Compiler generates migration SQL when version changes +-- Or errors if migration is ambiguous +``` + +#### API Versioning + +```lux +endpoint getUser: GET "/users/{id}" -> User @v2 + +-- Later: update endpoint +endpoint getUser: GET "/users/{id}" -> User @v3 + -- Compiler: "Breaking change for clients expecting @v2" + -- Must either: + -- 1. Keep old endpoint as getUser_v2 + -- 2. Prove @v3 is wire-compatible with @v2 +``` + +#### Compatibility Checking + +```lux +-- Compile-time compatibility proof +assert User @v2 compatibleWith User @v1 -- passes +assert User @v3 compatibleWith User @v1 -- fails: breaking change + +-- Generate compatibility report +lux schema diff User @v1 User @v3 +-- Output: +-- - 'name' renamed to 'fullName' (breaking) +-- - 'createdAt' added with default (compatible) +``` + +### Schema Semantics + +- **Versions are types**: `User @v1` and `User @v2` are distinct types +- **Migrations are functions**: `from @v1` is a function `@v1 -> @v2` +- **Compatibility is decidable**: Compiler checks all rules statically +- **Wire format is stable**: Version tag + canonical encoding + +--- + +## 3. Behavioral Types + +### Overview + +Properties beyond input/output types. Express and verify behavioral guarantees like purity, totality, idempotency. + +### Core Concepts + +#### Built-in Properties + +```lux +-- Purity: no effects +fn add(a: Int, b: Int): Int + is pure += a + b + +-- Totality: always terminates, no exceptions +fn safeDiv(a: Int, b: Int): Option + is total += if b == 0 then None else Some(a / b) + +-- Idempotency: f(f(x)) == f(x) +fn normalize(s: String): String + is idempotent += s.trim.lowercase + +-- Determinism: same inputs -> same outputs +fn hash(data: Bytes): Hash + is deterministic +``` + +#### Refinement Types + +```lux +type PositiveInt = Int where self > 0 +type NonEmptyList = List where self.len > 0 +type Email = String where self.matches(emailRegex) + +fn head(list: NonEmptyList): T + is total -- can't fail: list is non-empty += list.unsafeHead + +fn sqrt(n: PositiveInt): Float + is total -- can't fail: n is positive +``` + +#### Output Refinements + +```lux +fn sort(list: List): List + is pure, + is total, + where result.len == list.len, + where result.isSorted, + where result.isPermutationOf(list) + +fn filter(list: List, pred: fn(T): Bool): List + is pure, + is total, + where result.len <= list.len, + where result.all(pred) +``` + +#### Property Requirements + +```lux +-- Require properties from function arguments +fn retry(action: F, times: Int): Result + where F: fn(): T with {Fail}, + where F is idempotent -- enforced at call site! += ... + +fn memoize(f: F): fn(A): B with {Cache} + where F: fn(A): B, + where F is pure, + where F is deterministic += ... + +fn parallelize(actions: List): List with {Async} + where F: fn(): T, + where F is commutative -- order-independent += ... +``` + +### Verification Levels + +#### Level 1: Compiler-Proven + +Simple properties proven automatically: + +```lux +fn double(x: Int): Int + is pure -- proven: no effects += x * 2 + +fn always42(): Int + is total, -- proven: no recursion, no failure + is deterministic -- proven: no effects += 42 +``` + +#### Level 2: SMT-Backed + +Refinements checked by SMT solver: + +```lux +fn clamp(x: Int, lo: Int, hi: Int): Int + where lo <= hi, + where result >= lo, + where result <= hi += if x < lo then lo else if x > hi then hi else x +-- SMT proves postconditions hold +``` + +#### Level 3: Property-Tested + +Complex properties generate tests: + +```lux +fn sort(list: List): List + where result.isPermutationOf(list) -- too complex for SMT +-- Compiler generates: forall lists, sort(list).isPermutationOf(list) +-- Runs as property-based test +``` + +#### Level 4: Assumed + +Escape hatch for unverifiable properties: + +```lux +fn externalSort(list: List): List + assume is idempotent -- trust me (FFI, etc.) += ffiSort(list) +``` + +### Property Propagation + +Properties flow through composition: + +```lux +fn f(x: Int): Int is pure = x + 1 +fn g(x: Int): Int is pure = x * 2 + +fn h(x: Int): Int is pure = f(g(x)) -- inferred pure +``` + +```lux +fn f(x: Int): Int is idempotent = x.abs +fn g(x: Int): Int is idempotent = x.abs -- same function + +-- Composition of idempotent functions is idempotent IF they're the same +-- or if one is a fixpoint of the other. Otherwise, not guaranteed. +fn h(x: Int): Int = f(g(x)) -- NOT automatically idempotent +``` + +--- + +## 4. Type System Foundation + +### Core Types + +```lux +-- Primitives +Int, Int8, Int16, Int32, Int64 +UInt, UInt8, UInt16, UInt32, UInt64 +Float, Float32, Float64 +Bool +Char +String + +-- Collections +List +Set +Map +Array -- fixed size + +-- Optionality +Option = None | Some(T) +Result = Err(E) | Ok(T) + +-- Tuples +(A, B), (A, B, C), ... + +-- Records +{ name: String, age: Int } + +-- Functions +fn(A): B +fn(A, B): C +fn(A): B with {Effects} +``` + +### Algebraic Data Types + +```lux +type Color = Red | Green | Blue + +type Tree = + | Leaf(T) + | Node(Tree, Tree) + +type Result = + | Ok(T) + | Err(E) +``` + +### Pattern Matching + +```lux +fn describe(color: Color): String = + match color { + Red => "red", + Green => "green", + Blue => "blue" + } + +fn sum(tree: Tree): Int = + match tree { + Leaf(n) => n, + Node(left, right) => sum(left) + sum(right) + } +``` + +### Type Classes / Traits + +```lux +trait Eq { + fn eq(self, other: Self): Bool +} + +trait Ord: Eq { + fn cmp(self, other: Self): Ordering +} + +trait Show { + fn show(self): String +} + +impl Eq for Int { + fn eq(self, other) = intEq(self, other) +} +``` + +### Row Polymorphism + +```lux +-- Extensible records +fn getName(r: { name: String, ..rest }): String = r.name + +-- Works with any record containing 'name' +getName({ name: "Alice", age: 30 }) +getName({ name: "Bob", email: "bob@example.com" }) + +-- Extensible variants +type HttpError = { NotFound | Timeout | ..rest } +``` + +--- + +## 5. Implementation Roadmap + +### Phase 0: Foundation + +**Goal**: Minimal viable compiler + +- [ ] Lexer and parser for core syntax +- [ ] AST representation +- [ ] Basic type checker (no effects, no versions, no properties) +- [ ] Interpreter for testing semantics +- [ ] REPL + +**Deliverable**: Can type-check and interpret pure functional programs + +```lux +fn fib(n: Int): Int = + if n <= 1 then n else fib(n-1) + fib(n-2) +``` + +### Phase 1: Effect System + +**Goal**: First-class algebraic effects + +- [ ] Effect declarations +- [ ] Effect signatures on functions +- [ ] Handler definitions +- [ ] `run ... with` syntax +- [ ] Effect inference within function bodies +- [ ] Effect polymorphism +- [ ] Built-in effects (Fail, State, etc.) + +**Deliverable**: Can define, handle, and compose effects + +```lux +effect Console { fn print(s: String): Unit } + +fn greet(name: String): Unit with {Console} = + Console.print("Hello, {name}!") + +fn main() = + run greet("World") with { Console = stdoutConsole } +``` + +### Phase 2: Code Generation + +**Goal**: Compile to a real target + +- [ ] IR design (effect-aware) +- [ ] Backend selection (LLVM, WASM, or JS) +- [ ] Effect handler compilation (CPS or evidence-passing) +- [ ] Optimization passes +- [ ] Runtime library + +**Deliverable**: Compiled programs that run natively or in browser + +### Phase 3: Schema Evolution + +**Goal**: Versioned types with migrations + +- [ ] Version annotations on types (`@v1`, `@v2`) +- [ ] Compatibility checker +- [ ] Migration syntax (`from @v1 = ...`) +- [ ] Migration chaining +- [ ] Codec generation +- [ ] Version constraints (`@v2+`, `@latest`) + +**Deliverable**: Types with automatic serialization and migration + +```lux +type Config @v1 { host: String } +type Config @v2 { host: String, port: Int, from @v1 = { port: 8080, ..v1 } } + +let cfg: Config @v2 = Codec.decode(legacyBytes) +``` + +### Phase 4: Behavioral Types + +**Goal**: Property specifications and verification + +- [ ] Property syntax (`is pure`, `where result > 0`) +- [ ] Built-in properties (pure, total, idempotent, etc.) +- [ ] Refinement type checking +- [ ] SMT solver integration (Z3) +- [ ] Property-based test generation +- [ ] Property inference for simple cases +- [ ] `assume` escape hatch + +**Deliverable**: Compile-time verification of behavioral properties + +```lux +fn abs(x: Int): Int + is pure, + is total, + where result >= 0 += if x < 0 then -x else x +``` + +### Phase 5: Ecosystem + +**Goal**: Usable for real projects + +- [ ] Package manager +- [ ] Standard library +- [ ] LSP server (IDE support) +- [ ] Documentation generator +- [ ] REPL improvements +- [ ] Debugger +- [ ] Profiler + +### Phase 6: Advanced Features + +**Goal**: Full language vision + +- [ ] Database effect with schema-aware queries +- [ ] HTTP effect with API versioning +- [ ] Incremental computation (bonus feature) +- [ ] Distributed effects (location-aware) +- [ ] Proof assistant mode (optional full verification) + +--- + +## Open Design Questions + +### Syntax + +- [ ] Significant whitespace vs braces? +- [ ] Effect syntax: `with {E1, E2}` vs `!E1 + E2` vs ``? +- [ ] Version syntax: `@v1` vs `v1` vs `#1`? + +### Semantics + +- [ ] Effect handler semantics: deep vs shallow handlers? +- [ ] Version compatibility: structural or nominal? +- [ ] Property verification: sound or best-effort? + +### Pragmatics + +- [ ] Primary compile target: native, WASM, JS? +- [ ] Interop story: FFI design? +- [ ] Gradual adoption: can you use Lux from other languages? + +--- + +## References + +### Effect Systems +- Koka language (Daan Leijen) +- Eff language (Matija Pretnar) +- "Algebraic Effects for Functional Programming" (Daan Leijen) +- Frank language (Sam Lindley) + +### Schema Evolution +- Protocol Buffers / Protobuf +- Apache Avro +- "Schema Evolution in Heterogeneous Data Environments" + +### Behavioral Types +- Liquid Haskell (refinement types) +- F* (dependent types + effects) +- Dafny (verification) +- "Refinement Types for Haskell" (Vazou et al.) + +### General +- "Types and Programming Languages" (Pierce) +- "Practical Foundations for Programming Languages" (Harper) diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md new file mode 100644 index 0000000..d17eda8 --- /dev/null +++ b/docs/OVERVIEW.md @@ -0,0 +1,301 @@ +# Lux Language Overview + +## What is Lux? + +Lux is a statically-typed functional programming language with **algebraic effects** as a first-class feature. It makes side effects explicit, trackable, and testable. + +## What Can You Do With It? + +### Currently Working + +```lux +// Functions with type inference +fn factorial(n: Int): Int = + if n <= 1 then 1 else n * factorial(n - 1) + +// Higher-order functions +fn apply(f: fn(Int): Int, x: Int): Int = f(x) +fn double(x: Int): Int = x * 2 +let result = apply(double, 21) // 42 + +// Lambdas and closures +let add = fn(a: Int, b: Int): Int => a + b +let addFive = fn(x: Int): Int => add(5, x) + +// Pattern matching +fn describe(n: Int): String = + match n { + 0 => "zero", + 1 => "one", + _ => "many" + } + +// Records +let person = { name: "Alice", age: 30 } +let age = person.age + +// Tuples +let point = (10, 20) + +// Lists +let numbers = [1, 2, 3, 4, 5] + +// Pipe operator +let result = 5 |> double |> addOne // (5 * 2) + 1 = 11 + +// Built-in effects (Console, Fail) +Console.print("Hello, world!") + +// Custom effects +effect Logger { + fn log(level: String, msg: String): Unit +} + +// Effect handlers +handler consoleLogger: Logger { + fn log(level, msg) = Console.print("[" + level + "] " + msg) +} + +// Running with handlers +fn greet(name: String): Unit with {Logger} = + Logger.log("info", "Hello, " + name) + +run greet("Alice") with { Logger = consoleLogger } +``` + +### Standard Library (Built-in) + +```lux +// List operations +List.map([1, 2, 3], fn(x: Int): Int => x * 2) // [2, 4, 6] +List.filter([1, 2, 3, 4], fn(x: Int): Bool => x > 2) // [3, 4] +List.fold([1, 2, 3], 0, fn(acc: Int, x: Int): Int => acc + x) // 6 +List.head([1, 2, 3]) // Some(1) +List.tail([1, 2, 3]) // Some([2, 3]) +List.concat([1, 2], [3]) // [1, 2, 3] +List.reverse([1, 2, 3]) // [3, 2, 1] +List.length([1, 2, 3]) // 3 +List.get([1, 2, 3], 0) // Some(1) +List.range(0, 5) // [0, 1, 2, 3, 4] + +// String operations +String.split("a,b,c", ",") // ["a", "b", "c"] +String.join(["a", "b"], "-") // "a-b" +String.trim(" hello ") // "hello" +String.contains("hello", "ell") // true +String.replace("hi", "i", "ey") // "hey" +String.length("hello") // 5 +String.chars("hi") // ['h', 'i'] +String.lines("a\nb") // ["a", "b"] + +// Option operations +let x = Some(42) +let y = None +Option.map(x, fn(n: Int): Int => n * 2) // Some(84) +Option.flatMap(x, fn(n: Int): Option => Some(n + 1)) // Some(43) +Option.getOrElse(y, 0) // 0 +Option.isSome(x) // true +Option.isNone(y) // true + +// Result operations +let ok = Ok(42) +let err = Err("failed") +Result.map(ok, fn(n: Int): Int => n * 2) // Ok(84) +Result.getOrElse(err, 0) // 0 +Result.isOk(ok) // true +Result.isErr(err) // true + +// Utility functions +print("Hello") // prints to stdout +toString(42) // "42" +typeOf([1, 2, 3]) // "List" +``` + +### Planned (Not Yet Implemented) + +- **Schema Evolution**: Versioned types with automatic migrations +- **Behavioral Types**: Properties like `is pure`, `is idempotent` +- **Modules/Imports**: Code organization +- **Compilation**: Currently interpreter-only + +--- + +## Primary Use Cases + +### 1. Learning Effect Systems +Lux is an excellent educational tool for understanding algebraic effects without the complexity of Haskell's monad transformers or the academic syntax of languages like Koka. + +### 2. Testable Application Code +Effects make dependencies explicit. Swap handlers for testing: + +```lux +// Production +run app() with { Database = postgres, Http = realHttp } + +// Testing +run app() with { Database = mockDb, Http = mockHttp } +``` + +### 3. Domain Modeling +Explicit effects document what code can do: + +```lux +fn processOrder(order: Order): Receipt with {Database, Email, Logger} +// ^ The signature tells you exactly what side effects this function performs +``` + +### 4. Prototyping +Quick iteration with type inference and a REPL. + +--- + +## Pros and Cons + +### Pros + +| Advantage | Description | +|-----------|-------------| +| **Explicit Effects** | Function signatures show what side effects are possible | +| **Testability** | Swap effect handlers for mocking—no dependency injection frameworks | +| **Type Safety** | Static types catch errors at compile time | +| **Type Inference** | Write less type annotations, compiler figures it out | +| **Clean Syntax** | ML-family inspired, minimal boilerplate | +| **Pattern Matching** | Destructure data elegantly | +| **Immutable by Default** | Easier to reason about | +| **REPL** | Interactive development | + +### Cons + +| Limitation | Description | +|------------|-------------| +| **Interpreter Only** | No compilation to native/JS/WASM yet | +| **No Modules** | Can't split code across files | +| **Limited IO** | Only Console built-in, no file/network | +| **No Generics** | Polymorphic functions not fully implemented | +| **New Paradigm** | Effects require learning new concepts | +| **Small Ecosystem** | No packages, libraries, or community | +| **Early Stage** | Bugs likely, features incomplete | + +--- + +## Complexity Assessment + +### Conceptual Complexity + +| Concept | Difficulty | Notes | +|---------|------------|-------| +| Basic syntax | Easy | Similar to other ML-family languages | +| Functions | Easy | Standard functional style | +| Pattern matching | Easy | If you know any FP language | +| Type system | Medium | Hindley-Milner inference helps | +| Effects | Medium | New concept, but simpler than monads | +| Handlers | Medium | Requires understanding of continuations | + +### Comparison to Other Languages + +| Language | Complexity | Comparison to Lux | +|----------|------------|-------------------| +| Python | Simpler | No types, no effect tracking | +| TypeScript | Similar | Lux has effects, TS has larger ecosystem | +| Elm | Similar | Both pure FP, Lux has general effects | +| Haskell | More Complex | Monads harder than algebraic effects | +| Koka | Similar | Koka more academic, Lux more practical syntax | +| Rust | More Complex | Ownership adds significant complexity | + +### Learning Curve + +**Beginner** (1-2 hours): +- Basic expressions, functions, let bindings +- If/else, pattern matching +- REPL usage + +**Intermediate** (1-2 days): +- Custom types and records +- Higher-order functions +- Built-in effects (Console) + +**Advanced** (1 week): +- Custom effect definitions +- Effect handlers +- Understanding when to use effects vs. regular functions + +--- + +## When to Use Lux + +### Good Fit + +- Learning algebraic effects +- Prototyping with explicit effect tracking +- Small tools where testability matters +- Teaching functional programming concepts + +### Not a Good Fit (Yet) + +- Production applications (too early) +- Performance-critical code (interpreter) +- Large codebases (no modules) +- Web development (no JS compilation) +- Systems programming (no low-level control) + +--- + +## Example Session + +``` +$ cargo run +Lux v0.1.0 +Type :help for help, :quit to exit + +lux> let x = 42 +lux> x * 2 +84 +lux> fn greet(name: String): Unit with {Console} = Console.print("Hello, " + name) +lux> greet("World") +Hello, World +() +lux> let nums = [1, 2, 3] +lux> nums +[1, 2, 3] +lux> :quit +``` + +--- + +## Architecture + +``` +Source Code + │ + ▼ +┌─────────┐ +│ Lexer │ → Tokens +└─────────┘ + │ + ▼ +┌─────────┐ +│ Parser │ → AST +└─────────┘ + │ + ▼ +┌─────────────┐ +│ Type Checker│ → Typed AST + Effect Tracking +└─────────────┘ + │ + ▼ +┌─────────────┐ +│ Interpreter │ → Values + Effect Handling +└─────────────┘ +``` + +--- + +## Future Roadmap + +1. **Standard Library** - List, String, Option utilities +2. **Module System** - Import/export, namespaces +3. **JavaScript Backend** - Run in browsers +4. **Schema Evolution** - Versioned types +5. **Behavioral Types** - is pure, is idempotent +6. **LSP Server** - IDE support +7. **Package Manager** - Share code diff --git a/docs/VISION.md b/docs/VISION.md new file mode 100644 index 0000000..a327165 --- /dev/null +++ b/docs/VISION.md @@ -0,0 +1,253 @@ +# Lux: Vision and Roadmap + +## The Problems Lux Solves + +### 1. The "What Can This Code Do?" Problem + +In most languages, you can't tell from a function signature what it might do: + +```typescript +// TypeScript - what does this do? No idea without reading the code. +function processOrder(order: Order): Receipt { ... } +``` + +Could it hit a database? Send emails? Log? Throw? You don't know until you read every line (and every function it calls). + +**Lux solution:** + +```lux +fn processOrder(order: Order): Receipt with {Database, Email, Logger, Fail} +``` + +The signature *is* the documentation. Code review becomes "should this function really send emails?" Effects are compile-time checked. + +### 2. The Testing Problem + +Testing side-effecting code requires mocking frameworks, dependency injection containers, and boilerplate: + +```typescript +// TypeScript - need DI framework, mock libraries, setup/teardown +const mockDb = jest.mock('./database'); +const mockEmail = jest.mock('./email'); +// ... 50 lines of setup +``` + +**Lux solution:** + +```lux +// Production +run processOrder(order) with { + Database = postgres(connString), + Email = sendgrid(apiKey), + Logger = cloudWatch +} + +// Test - same code, different handlers +run processOrder(order) with { + Database = inMemoryDb(testData), + Email = collectEmails(sentList), // captures instead of sends + Logger = nullLogger +} +``` + +No mocking library. No DI framework. Just swap handlers. + +### 3. The Schema Evolution Problem (Planned) + +Types change. Data persists. Every production system eventually faces: +- "I renamed this field, now deserialization breaks" +- "I added a required field, old data can't load" +- "I need to migrate 10M rows and pray" + +**Lux solution:** + +```lux +type User @v1 { name: String, email: String } + +type User @v2 { + name: String, + email: String, + createdAt: Timestamp, + from @v1 = { createdAt: Timestamp.epoch(), ..v1 } // migration +} + +type User @v3 { + fullName: String, // renamed + email: String, + createdAt: Timestamp, + from @v2 = { fullName: v2.name, ..v2 } +} + +// Compiler knows: v1 → v2 is auto-compatible, v2 → v3 needs migration +// Serialization handles any version automatically +``` + +### 4. The "Is This Safe?" Problem (Planned) + +Critical properties are documented in comments and hoped for: + +```typescript +// IMPORTANT: This function must be idempotent for retry logic! +function chargeCard(payment: Payment): Result { ... } +``` + +**Lux solution:** + +```lux +fn chargeCard(payment: Payment): Result + is idempotent // Compiler enforces or generates property tests +``` + +```lux +fn retry(action: F, times: Int): Result + where F is idempotent // Won't compile if you pass non-idempotent function +``` + +--- + +## What's Built vs. What's Needed + +### Currently Working (Phase 1: Core Language) + +| Feature | Status | Notes | +|---------|--------|-------| +| Lexer/Parser | Done | Full syntax support | +| Type Inference | Done | Hindley-Milner | +| Functions/Closures | Done | First-class functions | +| Pattern Matching | Done | Destructuring, guards | +| Records/Tuples/Lists | Done | Basic data structures | +| Effect Declarations | Done | `effect Name { ... }` | +| Effect Operations | Done | `Effect.operation()` | +| Effect Handlers | Done | `handler name: Effect { ... }` | +| Run with Handlers | Done | `run expr with { ... }` | +| Built-in Console/Fail | Done | Basic IO | +| REPL | Done | Interactive development | +| Type Checking | Done | With effect tracking | + +### Needed for Real Use (Phase 2: Practical) + +| Feature | Effort | Why It Matters | +|---------|--------|----------------| +| **Module System** | 2-3 weeks | Can't build real apps without imports | +| **Standard Library** | Done | List.map, String.split, Option.map, etc. | +| **File/Network Effects** | 1-2 weeks | Real IO beyond Console | +| **Better Error Messages** | 2-3 weeks | Elm-quality diagnostics | +| **JS/WASM Compilation** | 4-6 weeks | Deploy to browsers/servers | + +### Needed for Full Vision (Phase 3: Differentiation) + +| Feature | Effort | Why It Matters | +|---------|--------|----------------| +| **Schema Evolution** | 4-6 weeks | The versioned types system | +| **Behavioral Types** | 4-6 weeks | is pure, is idempotent, etc. | +| **Effect Tracing/Debugging** | 2-3 weeks | Elm-like debugging | +| **LSP Server** | 3-4 weeks | IDE support | +| **Package Manager** | 2-3 weeks | Share code | + +--- + +## Elm-Style Debugging for Effects + +Elm's debugging is famous because: +1. **Time-travel**: See app state at any point +2. **No runtime crashes**: Everything is Result/Maybe +3. **Amazing error messages**: Context, suggestions, examples + +Lux can go further because effects are explicit: + +### Effect Tracing + +Every effect operation can be automatically logged: + +```lux +// With tracing enabled: +run processOrder(order) with { + Database = traced(postgres), // Logs all queries + Email = traced(sendgrid), // Logs all sends + Logger = traced(cloudWatch) // Meta-logging! +} + +// Output: +// [00:00:01] Database.query("SELECT * FROM users WHERE id = 42") +// [00:00:02] Database.query("SELECT * FROM inventory WHERE sku = 'ABC'") +// [00:00:03] Email.send(to: "customer@example.com", subject: "Order Confirmed") +// [00:00:03] Logger.log(level: "info", msg: "Order 123 processed") +``` + +### Effect Replay + +Since all effects are captured, we can replay: + +```lux +// Record effects during production +let recording = record(processOrder(order)) with { Database = postgres, ... } + +// Replay in development with exact same effect responses +replay(recording) with { Database = mockFromRecording(recording) } +``` + +### State Snapshots + +Since state changes only happen through effects: + +```lux +// Snapshot state before/after each effect +run debugSession(app) with { + State = snapshotted(initialState), // Captures every state change + Console = traced(stdout) +} + +// Later: inspect state at any point, step forward/backward +``` + +### Error Messages (To Build) + +Current: +``` +Type error at 15-45: Cannot unify Int with String +``` + +Goal (Elm-style): +``` +── TYPE MISMATCH ─────────────────────────────────────── src/order.lux + +The `calculateTotal` function expects an `Int` but got a `String`: + +15│ let total = calculateTotal(order.quantity) + ^^^^^^^^^^^^^^ + +`order.quantity` is a `String` but `calculateTotal` needs an `Int`. + +Hint: Maybe you need to parse the string? + + let qty = Int.parse(order.quantity)? + let total = calculateTotal(qty) +``` + +--- + +## Development Effort Summary + +**To be minimally useful for real projects:** +- Module system + standard library + better errors +- **Estimate: 6-8 weeks of focused work** + +**To deliver the full vision (effects + schemas + behavioral types):** +- All of the above + schema evolution + behavioral types + compilation +- **Estimate: 4-6 months of focused work** + +**To have Elm-quality experience:** +- All of the above + debugging tools + LSP + package manager +- **Estimate: 8-12 months of focused work** + +--- + +## Immediate Next Steps + +1. ~~**Standard Library**~~ - Done! List, String, Option, Result operations +2. **Module System** - `import`, `export`, namespaces +3. **File Effect** - `FileSystem.read`, `FileSystem.write` +4. **Error Message Overhaul** - Source snippets, suggestions, colors +5. **JavaScript Backend** - Compile to runnable JS + +These would make Lux usable for small real projects. diff --git a/examples/datatypes.lux b/examples/datatypes.lux new file mode 100644 index 0000000..8000bea --- /dev/null +++ b/examples/datatypes.lux @@ -0,0 +1,46 @@ +// Demonstrating algebraic data types and pattern matching + +// Define a binary tree +type Tree = + | Leaf(Int) + | Node(Tree, Tree) + +// Sum all values in a tree +fn sumTree(tree: Tree): Int = + match tree { + Leaf(n) => n, + Node(left, right) => sumTree(left) + sumTree(right) + } + +// Find the depth of a tree +fn depth(tree: Tree): Int = + match tree { + Leaf(_) => 1, + Node(left, right) => { + let leftDepth = depth(left) + let rightDepth = depth(right) + 1 + (if leftDepth > rightDepth then leftDepth else rightDepth) + } + } + +// Example tree: +// Node +// / \ +// Node Leaf(5) +// / \ +// Leaf(1) Leaf(2) + +let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5)) +let total = sumTree(myTree) +let treeDepth = depth(myTree) + +// Option type example +fn safeDivide(a: Int, b: Int): Option = + if b == 0 then None + else Some(a / b) + +fn showResult(result: Option): String = + match result { + None => "Division by zero!", + Some(n) => "Result: " + n + } diff --git a/examples/effects.lux b/examples/effects.lux new file mode 100644 index 0000000..3cee995 --- /dev/null +++ b/examples/effects.lux @@ -0,0 +1,35 @@ +// Demonstrating algebraic effects in Lux + +// Define a custom logging effect +effect Logger { + fn log(level: String, msg: String): Unit + fn getLevel(): String +} + +// A function that uses the Logger effect +fn processData(data: Int): Int with {Logger} = { + Logger.log("info", "Processing data...") + let result = data * 2 + Logger.log("debug", "Result computed") + result +} + +// A handler that prints logs to console +handler consoleLogger: Logger { + fn log(level, msg) = Console.print("[" + level + "] " + msg) + fn getLevel() = "debug" +} + +// A handler that ignores logs (for testing) +handler nullLogger: Logger { + fn log(level, msg) = () + fn getLevel() = "none" +} + +// Main function showing handler usage +fn main(): Unit with {Console} = { + let result = run processData(21) with { + Logger = consoleLogger + } + Console.print("Final result: " + result) +} diff --git a/examples/factorial.lux b/examples/factorial.lux new file mode 100644 index 0000000..8be2f68 --- /dev/null +++ b/examples/factorial.lux @@ -0,0 +1,12 @@ +// Factorial function demonstrating recursion + +fn factorial(n: Int): Int = + if n <= 1 then 1 + else n * factorial(n - 1) + +// Calculate factorial of 10 +let result = factorial(10) + +// Print result +fn main(): Unit with {Console} = + Console.print("10! = " + result) diff --git a/examples/functional.lux b/examples/functional.lux new file mode 100644 index 0000000..934bb7f --- /dev/null +++ b/examples/functional.lux @@ -0,0 +1,42 @@ +// Demonstrating functional programming features + +// Higher-order functions +fn apply(f: fn(Int): Int, x: Int): Int = f(x) + +fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = + fn(x: Int): Int => f(g(x)) + +// Basic functions +fn double(x: Int): Int = x * 2 +fn addOne(x: Int): Int = x + 1 +fn square(x: Int): Int = x * x + +// Using apply +let result1 = apply(double, 21) // 42 + +// Using compose +let doubleAndAddOne = compose(addOne, double) +let result2 = doubleAndAddOne(5) // 11 + +// Using the pipe operator +let result3 = 5 |> double |> addOne |> square // ((5 * 2) + 1)^2 = 121 + +// Currying example +fn add(a: Int): fn(Int): Int = + fn(b: Int): Int => a + b + +let add5 = add(5) +let result4 = add5(10) // 15 + +// Partial application simulation +fn multiply(a: Int, b: Int): Int = a * b + +let times3 = fn(x: Int): Int => multiply(3, x) +let result5 = times3(7) // 21 + +// Working with records +let transform = fn(record: { x: Int, y: Int }): Int => + record.x + record.y + +let point = { x: 10, y: 20 } +let sum = transform(point) // 30 diff --git a/examples/hello.lux b/examples/hello.lux new file mode 100644 index 0000000..3be0b7f --- /dev/null +++ b/examples/hello.lux @@ -0,0 +1,5 @@ +// Hello World in Lux +// Demonstrates basic effect usage + +fn main(): Unit with {Console} = + Console.print("Hello, World!") diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3e6d6b7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1770841267, + "narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1770952264, + "narHash": "sha256-CjymNrJZWBtpavyuTkfPVPaZkwzIzGaf0E/3WgcwM14=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "ec6a3d5cdf14bb5a1dd03652bd3f6351004d2188", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..31f0603 --- /dev/null +++ b/flake.nix @@ -0,0 +1,55 @@ +{ + description = "Lux - A functional programming language with first-class effects"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + rustToolchain + cargo-watch + cargo-edit + ]; + + RUST_BACKTRACE = "1"; + RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; + + shellHook = '' + printf "\n" + printf " \033[1;35m╦ ╦ ╦╦ ╦\033[0m\n" + printf " \033[1;35m║ ║ ║╔╣\033[0m\n" + printf " \033[1;35m╩═╝╚═╝╩ ╩\033[0m v0.1.0\n" + printf "\n" + printf " Functional language with first-class effects\n" + printf "\n" + printf " \033[1mCommands:\033[0m\n" + printf " cargo build Build the compiler\n" + printf " cargo run Start the REPL\n" + printf " cargo test Run tests\n" + printf " cargo run -- \033[3m\033[0m Run a file\n" + printf "\n" + ''; + }; + + packages.default = pkgs.rustPlatform.buildRustPackage { + pname = "lux"; + version = "0.1.0"; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; + }; + } + ); +} diff --git a/src/ast.rs b/src/ast.rs new file mode 100644 index 0000000..13b82d1 --- /dev/null +++ b/src/ast.rs @@ -0,0 +1,583 @@ +//! Abstract Syntax Tree for the Lux language + +#![allow(dead_code)] + +use std::fmt; + +/// Source location for error reporting +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl Span { + pub fn new(start: usize, end: usize) -> Self { + Self { start, end } + } + + pub fn merge(self, other: Span) -> Span { + Span { + start: self.start.min(other.start), + end: self.end.max(other.end), + } + } +} + +/// An identifier (variable or type name) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Ident { + pub name: String, + pub span: Span, +} + +impl Ident { + pub fn new(name: impl Into, span: Span) -> Self { + Self { + name: name.into(), + span, + } + } +} + +impl fmt::Display for Ident { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +/// Visibility modifier +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Visibility { + /// Public - exported from module + Public, + /// Private - only visible within module (default) + #[default] + Private, +} + +// ============ Schema Evolution ============ + +/// A version number for schema evolution (e.g., @v1, @v2) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Version { + pub number: u32, + pub span: Span, +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.number.cmp(&other.number) + } +} + +impl Version { + pub fn new(number: u32, span: Span) -> Self { + Self { number, span } + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "@v{}", self.number) + } +} + +/// Version constraint for type annotations +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VersionConstraint { + /// Exactly this version: @v2 + Exact(Version), + /// This version or later: @v2+ + AtLeast(Version), + /// Latest version: @latest + Latest(Span), +} + +impl fmt::Display for VersionConstraint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VersionConstraint::Exact(v) => write!(f, "{}", v), + VersionConstraint::AtLeast(v) => write!(f, "{}+", v), + VersionConstraint::Latest(_) => write!(f, "@latest"), + } + } +} + +/// Migration from one version to another +#[derive(Debug, Clone)] +pub struct Migration { + /// Source version: from @v1 + pub from_version: Version, + /// Migration body (expression that transforms old to new) + pub body: Expr, + pub span: Span, +} + +/// Module path: foo/bar/baz +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModulePath { + pub segments: Vec, + pub span: Span, +} + +impl ModulePath { + pub fn to_string(&self) -> String { + self.segments + .iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/") + } +} + +/// Import declaration +#[derive(Debug, Clone)] +pub struct ImportDecl { + /// The module path being imported + pub path: ModulePath, + /// Optional alias: import foo/bar as baz + pub alias: Option, + /// Specific items to import: import foo.{a, b, c} + pub items: Option>, + /// Import all items: import foo.* + pub wildcard: bool, + pub span: Span, +} + +/// A complete program (or module) +#[derive(Debug, Clone)] +pub struct Program { + /// Module imports + pub imports: Vec, + /// Top-level declarations + pub declarations: Vec, +} + +/// Top-level declarations +#[derive(Debug, Clone)] +pub enum Declaration { + /// Function definition: fn name(params): ReturnType with {Effects} = body + Function(FunctionDecl), + /// Effect declaration: effect Name { fn op1(...): T, ... } + Effect(EffectDecl), + /// Type alias or ADT: type Name = ... + Type(TypeDecl), + /// Handler definition: handler name: Effect { ... } + Handler(HandlerDecl), + /// Let binding at top level + Let(LetDecl), +} + +/// Function declaration +#[derive(Debug, Clone)] +pub struct FunctionDecl { + pub visibility: Visibility, + pub name: Ident, + pub type_params: Vec, + pub params: Vec, + pub return_type: TypeExpr, + pub effects: Vec, + pub body: Expr, + pub span: Span, +} + +/// Function parameter +#[derive(Debug, Clone)] +pub struct Parameter { + pub name: Ident, + pub typ: TypeExpr, + pub span: Span, +} + +/// Effect declaration +#[derive(Debug, Clone)] +pub struct EffectDecl { + pub name: Ident, + pub type_params: Vec, + pub operations: Vec, + pub span: Span, +} + +/// An operation within an effect +#[derive(Debug, Clone)] +pub struct EffectOp { + pub name: Ident, + pub params: Vec, + pub return_type: TypeExpr, + pub span: Span, +} + +/// Type declaration (alias or ADT) +#[derive(Debug, Clone)] +pub struct TypeDecl { + pub visibility: Visibility, + pub name: Ident, + pub type_params: Vec, + /// Optional version annotation: type User @v2 { ... } + pub version: Option, + pub definition: TypeDef, + /// Migrations from previous versions: from @v1 = { ... } + pub migrations: Vec, + pub span: Span, +} + +/// Type definition +#[derive(Debug, Clone)] +pub enum TypeDef { + /// Type alias: type Foo = Bar + Alias(TypeExpr), + /// Record type: type Foo { field: Type, ... } + Record(Vec), + /// Enum/ADT: type Foo = A | B(Int) | C { x: Int } + Enum(Vec), +} + +/// Record field +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecordField { + pub name: Ident, + pub typ: TypeExpr, + pub span: Span, +} + +/// Enum variant +#[derive(Debug, Clone)] +pub struct Variant { + pub name: Ident, + pub fields: VariantFields, + pub span: Span, +} + +/// Variant field types +#[derive(Debug, Clone)] +pub enum VariantFields { + /// Unit variant: A + Unit, + /// Tuple variant: A(Int, String) + Tuple(Vec), + /// Record variant: A { x: Int, y: String } + Record(Vec), +} + +/// Handler declaration +#[derive(Debug, Clone)] +pub struct HandlerDecl { + pub name: Ident, + pub params: Vec, + pub effect: Ident, + pub implementations: Vec, + pub span: Span, +} + +/// Implementation of an effect operation in a handler +#[derive(Debug, Clone)] +pub struct HandlerImpl { + pub op_name: Ident, + pub params: Vec, + pub resume: Option, // The continuation parameter + pub body: Expr, + pub span: Span, +} + +/// Let declaration +#[derive(Debug, Clone)] +pub struct LetDecl { + pub visibility: Visibility, + pub name: Ident, + pub typ: Option, + pub value: Expr, + pub span: Span, +} + +/// Type expressions +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypeExpr { + /// Named type: Int, String, List + Named(Ident), + /// Generic type application: List, Map + App(Box, Vec), + /// Function type: fn(A, B): C + Function { + params: Vec, + return_type: Box, + effects: Vec, + }, + /// Tuple type: (A, B, C) + Tuple(Vec), + /// Record type: { name: String, age: Int } + Record(Vec), + /// Unit type + Unit, + /// Versioned type: User @v2, User @v2+, User @latest + Versioned { + base: Box, + constraint: VersionConstraint, + }, +} + +impl TypeExpr { + pub fn named(name: &str) -> Self { + TypeExpr::Named(Ident::new(name, Span::default())) + } +} + +/// Expressions +#[derive(Debug, Clone)] +pub enum Expr { + /// Literal values + Literal(Literal), + /// Variable reference + Var(Ident), + /// Binary operation: a + b + BinaryOp { + op: BinaryOp, + left: Box, + right: Box, + span: Span, + }, + /// Unary operation: -a, !a + UnaryOp { + op: UnaryOp, + operand: Box, + span: Span, + }, + /// Function call: foo(a, b) + Call { + func: Box, + args: Vec, + span: Span, + }, + /// Effect operation call: Effect.operation(args) + EffectOp { + effect: Ident, + operation: Ident, + args: Vec, + span: Span, + }, + /// Field access: foo.bar + Field { + object: Box, + field: Ident, + span: Span, + }, + /// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1 + Lambda { + params: Vec, + return_type: Option>, + effects: Vec, + body: Box, + span: Span, + }, + /// Let binding: let x = e1; e2 + Let { + name: Ident, + typ: Option, + value: Box, + body: Box, + span: Span, + }, + /// If expression: if cond then e1 else e2 + If { + condition: Box, + then_branch: Box, + else_branch: Box, + span: Span, + }, + /// Match expression + Match { + scrutinee: Box, + arms: Vec, + span: Span, + }, + /// Block: { e1; e2; e3 } + Block { + statements: Vec, + result: Box, + span: Span, + }, + /// Record literal: { name: "Alice", age: 30 } + Record { + fields: Vec<(Ident, Expr)>, + span: Span, + }, + /// Tuple literal: (1, "hello", true) + Tuple { elements: Vec, span: Span }, + /// List literal: [1, 2, 3] + List { elements: Vec, span: Span }, + /// Run with handlers: run expr with { Effect = handler, ... } + Run { + expr: Box, + handlers: Vec<(Ident, Expr)>, + span: Span, + }, + /// Resume continuation in handler (like calling the continuation) + Resume { value: Box, span: Span }, +} + +impl Expr { + pub fn span(&self) -> Span { + match self { + Expr::Literal(lit) => lit.span, + Expr::Var(ident) => ident.span, + Expr::BinaryOp { span, .. } => *span, + Expr::UnaryOp { span, .. } => *span, + Expr::Call { span, .. } => *span, + Expr::EffectOp { span, .. } => *span, + Expr::Field { span, .. } => *span, + Expr::Lambda { span, .. } => *span, + Expr::Let { span, .. } => *span, + Expr::If { span, .. } => *span, + Expr::Match { span, .. } => *span, + Expr::Block { span, .. } => *span, + Expr::Record { span, .. } => *span, + Expr::Tuple { span, .. } => *span, + Expr::List { span, .. } => *span, + Expr::Run { span, .. } => *span, + Expr::Resume { span, .. } => *span, + } + } +} + +/// Literal values +#[derive(Debug, Clone)] +pub struct Literal { + pub kind: LiteralKind, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub enum LiteralKind { + Int(i64), + Float(f64), + String(String), + Char(char), + Bool(bool), + Unit, +} + +/// Binary operators +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOp { + // Arithmetic + Add, + Sub, + Mul, + Div, + Mod, + // Comparison + Eq, + Ne, + Lt, + Le, + Gt, + Ge, + // Logical + And, + Or, + // Other + Pipe, // |> +} + +impl fmt::Display for BinaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BinaryOp::Add => write!(f, "+"), + BinaryOp::Sub => write!(f, "-"), + BinaryOp::Mul => write!(f, "*"), + BinaryOp::Div => write!(f, "/"), + BinaryOp::Mod => write!(f, "%"), + BinaryOp::Eq => write!(f, "=="), + BinaryOp::Ne => write!(f, "!="), + BinaryOp::Lt => write!(f, "<"), + BinaryOp::Le => write!(f, "<="), + BinaryOp::Gt => write!(f, ">"), + BinaryOp::Ge => write!(f, ">="), + BinaryOp::And => write!(f, "&&"), + BinaryOp::Or => write!(f, "||"), + BinaryOp::Pipe => write!(f, "|>"), + } + } +} + +/// Unary operators +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnaryOp { + Neg, // - + Not, // ! +} + +impl fmt::Display for UnaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UnaryOp::Neg => write!(f, "-"), + UnaryOp::Not => write!(f, "!"), + } + } +} + +/// Statement in a block +#[derive(Debug, Clone)] +pub enum Statement { + /// Expression statement + Expr(Expr), + /// Let binding without body (in blocks) + Let { + name: Ident, + typ: Option, + value: Expr, + span: Span, + }, +} + +/// Match arm +#[derive(Debug, Clone)] +pub struct MatchArm { + pub pattern: Pattern, + pub guard: Option, + pub body: Expr, + pub span: Span, +} + +/// Patterns for matching +#[derive(Debug, Clone)] +pub enum Pattern { + /// Wildcard: _ + Wildcard(Span), + /// Variable binding: x + Var(Ident), + /// Literal: 42, "hello", true + Literal(Literal), + /// Constructor: Some(x), None, Ok(v) + Constructor { + name: Ident, + fields: Vec, + span: Span, + }, + /// Record pattern: { name, age: a } + Record { + fields: Vec<(Ident, Pattern)>, + span: Span, + }, + /// Tuple pattern: (a, b, c) + Tuple { elements: Vec, span: Span }, +} + +impl Pattern { + pub fn span(&self) -> Span { + match self { + Pattern::Wildcard(span) => *span, + Pattern::Var(ident) => ident.span, + Pattern::Literal(lit) => lit.span, + Pattern::Constructor { span, .. } => *span, + Pattern::Record { span, .. } => *span, + Pattern::Tuple { span, .. } => *span, + } + } +} diff --git a/src/interpreter.rs b/src/interpreter.rs new file mode 100644 index 0000000..cc7153e --- /dev/null +++ b/src/interpreter.rs @@ -0,0 +1,2202 @@ +//! Tree-walking interpreter for the Lux language with algebraic effects + +#![allow(dead_code, unused_variables)] + +use crate::ast::*; +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt; +use std::rc::Rc; + +/// Built-in function identifier +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BuiltinFn { + // List operations + ListMap, + ListFilter, + ListFold, + ListHead, + ListTail, + ListConcat, + ListReverse, + ListLength, + ListGet, + ListRange, + + // String operations + StringSplit, + StringJoin, + StringTrim, + StringContains, + StringReplace, + StringLength, + StringChars, + StringLines, + + // Option operations + OptionMap, + OptionFlatMap, + OptionGetOrElse, + OptionIsSome, + OptionIsNone, + + // Result operations + ResultMap, + ResultFlatMap, + ResultGetOrElse, + ResultIsOk, + ResultIsErr, + + // Utility + Print, + ToString, + TypeOf, +} + +/// Runtime value +#[derive(Debug, Clone)] +pub enum Value { + Int(i64), + Float(f64), + Bool(bool), + String(String), + Char(char), + Unit, + List(Vec), + Tuple(Vec), + Record(HashMap), + Function(Rc), + Handler(Rc), + /// Built-in function + Builtin(BuiltinFn), + /// Constructor value (for ADTs) + Constructor { + name: String, + fields: Vec, + }, + /// Versioned value (for schema evolution) + Versioned { + type_name: String, + version: u32, + value: Box, + }, +} + +impl Value { + pub fn type_name(&self) -> &'static str { + match self { + Value::Int(_) => "Int", + Value::Float(_) => "Float", + Value::Bool(_) => "Bool", + Value::String(_) => "String", + Value::Char(_) => "Char", + Value::Unit => "Unit", + Value::List(_) => "List", + Value::Tuple(_) => "Tuple", + Value::Record(_) => "Record", + Value::Function(_) => "Function", + Value::Handler(_) => "Handler", + Value::Builtin(_) => "Function", + Value::Constructor { .. } => "Constructor", + Value::Versioned { .. } => "Versioned", + } + } + + /// Unwrap a versioned value to get the inner value + pub fn unwrap_versioned(&self) -> &Value { + match self { + Value::Versioned { value, .. } => value.unwrap_versioned(), + other => other, + } + } + + /// Get version info if this is a versioned value + pub fn version_info(&self) -> Option<(String, u32)> { + match self { + Value::Versioned { + type_name, version, .. + } => Some((type_name.clone(), *version)), + _ => None, + } + } +} + +/// Trait for extracting typed values from Value +trait TryFromValue: Sized { + const TYPE_NAME: &'static str; + fn try_from_value(value: &Value) -> Option; +} + +impl TryFromValue for i64 { + const TYPE_NAME: &'static str = "Int"; + fn try_from_value(value: &Value) -> Option { + match value { + Value::Int(n) => Some(*n), + _ => None, + } + } +} + +impl TryFromValue for f64 { + const TYPE_NAME: &'static str = "Float"; + fn try_from_value(value: &Value) -> Option { + match value { + Value::Float(n) => Some(*n), + _ => None, + } + } +} + +impl TryFromValue for String { + const TYPE_NAME: &'static str = "String"; + fn try_from_value(value: &Value) -> Option { + match value { + Value::String(s) => Some(s.clone()), + _ => None, + } + } +} + +impl TryFromValue for bool { + const TYPE_NAME: &'static str = "Bool"; + fn try_from_value(value: &Value) -> Option { + match value { + Value::Bool(b) => Some(*b), + _ => None, + } + } +} + +impl TryFromValue for Vec { + const TYPE_NAME: &'static str = "List"; + fn try_from_value(value: &Value) -> Option { + match value { + Value::List(l) => Some(l.clone()), + _ => None, + } + } +} + +impl TryFromValue for Value { + const TYPE_NAME: &'static str = "any"; + fn try_from_value(value: &Value) -> Option { + Some(value.clone()) + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::Int(n) => write!(f, "{}", n), + Value::Float(n) => write!(f, "{}", n), + Value::Bool(b) => write!(f, "{}", b), + Value::String(s) => write!(f, "\"{}\"", s), + Value::Char(c) => write!(f, "'{}'", c), + Value::Unit => write!(f, "()"), + Value::List(elements) => { + write!(f, "[")?; + for (i, e) in elements.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", e)?; + } + write!(f, "]") + } + Value::Tuple(elements) => { + write!(f, "(")?; + for (i, e) in elements.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", e)?; + } + write!(f, ")") + } + Value::Record(fields) => { + write!(f, "{{ ")?; + for (i, (name, value)) in fields.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", name, value)?; + } + write!(f, " }}") + } + Value::Function(_) => write!(f, ""), + Value::Builtin(b) => write!(f, "", b), + Value::Handler(_) => write!(f, ""), + Value::Constructor { name, fields } => { + if fields.is_empty() { + write!(f, "{}", name) + } else { + write!(f, "{}(", name)?; + for (i, field) in fields.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", field)?; + } + write!(f, ")") + } + } + Value::Versioned { + type_name, + version, + value, + } => { + write!(f, "{} @v{}", value, version) + } + } + } +} + +/// Function closure +#[derive(Debug)] +pub struct Closure { + pub params: Vec, + pub body: Expr, + pub env: Env, +} + +/// Handler value +#[derive(Debug)] +pub struct HandlerValue { + pub effect: String, + pub implementations: HashMap, + pub env: Env, +} + +/// Environment (lexical scope) +#[derive(Debug, Clone, Default)] +pub struct Env { + bindings: Rc>>, + parent: Option>, +} + +impl Env { + pub fn new() -> Self { + Self { + bindings: Rc::new(RefCell::new(HashMap::new())), + parent: None, + } + } + + pub fn extend(&self) -> Self { + Self { + bindings: Rc::new(RefCell::new(HashMap::new())), + parent: Some(Box::new(self.clone())), + } + } + + pub fn define(&self, name: impl Into, value: Value) { + self.bindings.borrow_mut().insert(name.into(), value); + } + + pub fn get(&self, name: &str) -> Option { + if let Some(value) = self.bindings.borrow().get(name) { + return Some(value.clone()); + } + if let Some(ref parent) = self.parent { + return parent.get(name); + } + None + } +} + +/// Runtime error +#[derive(Debug, Clone)] +pub struct RuntimeError { + pub message: String, + pub span: Option, +} + +impl fmt::Display for RuntimeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(span) = self.span { + write!( + f, + "Runtime error at {}-{}: {}", + span.start, span.end, self.message + ) + } else { + write!(f, "Runtime error: {}", self.message) + } + } +} + +impl std::error::Error for RuntimeError {} + +/// Effect operation request +#[derive(Debug, Clone)] +pub struct EffectRequest { + pub effect: String, + pub operation: String, + pub args: Vec, + pub continuation: Continuation, +} + +/// Continuation (captured rest of computation) +#[derive(Debug, Clone)] +pub struct Continuation { + // For simplicity, we'll use a callback-based approach + // In a real implementation, this would capture the stack + id: usize, +} + +static NEXT_CONT_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + +impl Continuation { + fn new() -> Self { + Self { + id: NEXT_CONT_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), + } + } +} + +/// Result of evaluation (either a value or an effect request) +pub enum EvalResult { + Value(Value), + Effect(EffectRequest), +} + +/// Effect trace entry for debugging +#[derive(Debug, Clone)] +pub struct EffectTrace { + pub effect: String, + pub operation: String, + pub args: Vec, + pub result: Option, + pub timestamp_us: u128, +} + +/// The interpreter +/// A stored migration function +#[derive(Clone)] +pub struct StoredMigration { + /// The expression to evaluate for migration + pub body: Expr, + /// Environment captured when the migration was defined + pub env: Env, +} + +impl std::fmt::Debug for StoredMigration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StoredMigration").finish() + } +} + +pub struct Interpreter { + global_env: Env, + /// Stack of active effect handlers + handler_stack: Vec>, + /// Stored continuations for resumption + continuations: HashMap Result>>, + /// Effect tracing for debugging + pub trace_effects: bool, + /// Collected effect traces + pub effect_traces: Vec, + /// Start time for timestamps + start_time: std::time::Instant, + /// Migration registry: type_name -> (from_version -> to_version -> migration) + migrations: HashMap>, +} + +impl Interpreter { + pub fn new() -> Self { + let global_env = Env::new(); + + // Add built-in functions + Self::add_builtins(&global_env); + + Self { + global_env, + handler_stack: Vec::new(), + continuations: HashMap::new(), + trace_effects: false, + effect_traces: Vec::new(), + start_time: std::time::Instant::now(), + migrations: HashMap::new(), + } + } + + /// Enable effect tracing for debugging + pub fn enable_tracing(&mut self) { + self.trace_effects = true; + self.effect_traces.clear(); + self.start_time = std::time::Instant::now(); + } + + /// Get all effect traces + pub fn get_traces(&self) -> &[EffectTrace] { + &self.effect_traces + } + + /// Print effect traces in a readable format + pub fn print_traces(&self) { + println!("\n── EFFECT TRACE ──────────────────────────────────────"); + for trace in &self.effect_traces { + let time_ms = trace.timestamp_us as f64 / 1000.0; + let args_str: Vec = trace.args.iter().map(|a| format!("{}", a)).collect(); + let result_str = trace + .result + .as_ref() + .map(|r| format!(" → {}", r)) + .unwrap_or_default(); + println!( + "[{:8.3}ms] {}.{}({}){}", + time_ms, + trace.effect, + trace.operation, + args_str.join(", "), + result_str + ); + } + println!("──────────────────────────────────────────────────────\n"); + } + + /// Register a migration for a versioned type + pub fn register_migration( + &mut self, + type_name: &str, + from_version: u32, + migration: StoredMigration, + ) { + self.migrations + .entry(type_name.to_string()) + .or_default() + .insert(from_version, migration); + } + + /// Create a versioned value + pub fn create_versioned(&self, type_name: &str, version: u32, value: Value) -> Value { + Value::Versioned { + type_name: type_name.to_string(), + version, + value: Box::new(value), + } + } + + /// Migrate a versioned value to a target version + pub fn migrate_value( + &mut self, + value: Value, + target_version: u32, + ) -> Result { + let (type_name, current_version, inner_value) = match value { + Value::Versioned { + type_name, + version, + value, + } => (type_name, version, *value), + other => return Ok(other), // Non-versioned values don't need migration + }; + + if current_version == target_version { + return Ok(Value::Versioned { + type_name, + version: target_version, + value: Box::new(inner_value), + }); + } + + if current_version > target_version { + return Err(RuntimeError { + message: format!( + "Cannot downgrade {} from @v{} to @v{}", + type_name, current_version, target_version + ), + span: None, + }); + } + + // Migrate step by step: v1 -> v2 -> v3 -> ... -> target + let mut current_value = inner_value; + let mut current_ver = current_version; + + while current_ver < target_version { + let next_ver = current_ver + 1; + + // Look up the migration + let migration = self + .migrations + .get(&type_name) + .and_then(|m| m.get(¤t_ver)) + .cloned(); + + match migration { + Some(stored_migration) => { + // Execute the migration + let migration_env = stored_migration.env.clone(); + migration_env.define("old", current_value.clone()); + + current_value = self.eval_expr(&stored_migration.body, &migration_env)?; + current_ver = next_ver; + } + None => { + // No explicit migration - try auto-migration (just pass through) + // In a full implementation, we'd check compatibility here + current_ver = next_ver; + } + } + } + + Ok(Value::Versioned { + type_name, + version: target_version, + value: Box::new(current_value), + }) + } + + fn add_builtins(env: &Env) { + // Option constructors + env.define( + "None", + Value::Constructor { + name: "None".to_string(), + fields: Vec::new(), + }, + ); + env.define( + "Some", + Value::Constructor { + name: "Some".to_string(), + fields: Vec::new(), // Will accumulate args when called + }, + ); + + // Result constructors + env.define( + "Ok", + Value::Constructor { + name: "Ok".to_string(), + fields: Vec::new(), + }, + ); + env.define( + "Err", + Value::Constructor { + name: "Err".to_string(), + fields: Vec::new(), + }, + ); + + // List module (as a record with function fields) + let list_module = Value::Record(HashMap::from([ + ("map".to_string(), Value::Builtin(BuiltinFn::ListMap)), + ("filter".to_string(), Value::Builtin(BuiltinFn::ListFilter)), + ("fold".to_string(), Value::Builtin(BuiltinFn::ListFold)), + ("head".to_string(), Value::Builtin(BuiltinFn::ListHead)), + ("tail".to_string(), Value::Builtin(BuiltinFn::ListTail)), + ("concat".to_string(), Value::Builtin(BuiltinFn::ListConcat)), + ( + "reverse".to_string(), + Value::Builtin(BuiltinFn::ListReverse), + ), + ("length".to_string(), Value::Builtin(BuiltinFn::ListLength)), + ("get".to_string(), Value::Builtin(BuiltinFn::ListGet)), + ("range".to_string(), Value::Builtin(BuiltinFn::ListRange)), + ])); + env.define("List", list_module); + + // String module + let string_module = Value::Record(HashMap::from([ + ("split".to_string(), Value::Builtin(BuiltinFn::StringSplit)), + ("join".to_string(), Value::Builtin(BuiltinFn::StringJoin)), + ("trim".to_string(), Value::Builtin(BuiltinFn::StringTrim)), + ( + "contains".to_string(), + Value::Builtin(BuiltinFn::StringContains), + ), + ( + "replace".to_string(), + Value::Builtin(BuiltinFn::StringReplace), + ), + ( + "length".to_string(), + Value::Builtin(BuiltinFn::StringLength), + ), + ("chars".to_string(), Value::Builtin(BuiltinFn::StringChars)), + ("lines".to_string(), Value::Builtin(BuiltinFn::StringLines)), + ])); + env.define("String", string_module); + + // Option module (functions, not constructors) + let option_module = Value::Record(HashMap::from([ + ("map".to_string(), Value::Builtin(BuiltinFn::OptionMap)), + ( + "flatMap".to_string(), + Value::Builtin(BuiltinFn::OptionFlatMap), + ), + ( + "getOrElse".to_string(), + Value::Builtin(BuiltinFn::OptionGetOrElse), + ), + ( + "isSome".to_string(), + Value::Builtin(BuiltinFn::OptionIsSome), + ), + ( + "isNone".to_string(), + Value::Builtin(BuiltinFn::OptionIsNone), + ), + ])); + env.define("Option", option_module); + + // Result module + let result_module = Value::Record(HashMap::from([ + ("map".to_string(), Value::Builtin(BuiltinFn::ResultMap)), + ( + "flatMap".to_string(), + Value::Builtin(BuiltinFn::ResultFlatMap), + ), + ( + "getOrElse".to_string(), + Value::Builtin(BuiltinFn::ResultGetOrElse), + ), + ("isOk".to_string(), Value::Builtin(BuiltinFn::ResultIsOk)), + ("isErr".to_string(), Value::Builtin(BuiltinFn::ResultIsErr)), + ])); + env.define("Result", result_module); + + // Utility functions + env.define("print", Value::Builtin(BuiltinFn::Print)); + env.define("toString", Value::Builtin(BuiltinFn::ToString)); + env.define("typeOf", Value::Builtin(BuiltinFn::TypeOf)); + } + + /// Execute a program + pub fn run(&mut self, program: &Program) -> Result { + let mut last_value = Value::Unit; + + for decl in &program.declarations { + last_value = self.eval_declaration(decl)?; + } + + Ok(last_value) + } + + /// Execute a program with module support + pub fn run_with_modules( + &mut self, + program: &Program, + loader: &crate::modules::ModuleLoader, + ) -> Result { + // Process imports first + self.load_imports(&program.imports, loader)?; + + // Then run the declarations + self.run(program) + } + + /// Load imports into the environment + pub fn load_imports( + &mut self, + imports: &[ImportDecl], + loader: &crate::modules::ModuleLoader, + ) -> Result<(), RuntimeError> { + use crate::modules::ImportKind; + + let resolved = loader.resolve_imports(imports).map_err(|e| RuntimeError { + message: e.message, + span: None, + })?; + + for (name, import) in resolved { + match import.kind { + ImportKind::Module => { + // Import as a module object - create a record with all exports + let module = + loader + .get_module(&import.module_path) + .ok_or_else(|| RuntimeError { + message: format!("Module '{}' not found", import.module_path), + span: None, + })?; + + // Create a temporary interpreter to evaluate the module + // Clone the module.program to avoid borrow issues + let program = module.program.clone(); + let exports = module.exports.clone(); + + let mut module_interp = Interpreter::new(); + module_interp.run_with_modules(&program, loader)?; + + // Collect all public values into a record + let mut module_record = HashMap::new(); + for export_name in &exports { + if let Some(value) = module_interp.global_env.get(export_name) { + module_record.insert(export_name.clone(), value); + } + } + + self.global_env.define(&name, Value::Record(module_record)); + } + ImportKind::Direct => { + // Import a specific name directly + let module = + loader + .get_module(&import.module_path) + .ok_or_else(|| RuntimeError { + message: format!("Module '{}' not found", import.module_path), + span: None, + })?; + + // Clone the module data to avoid borrow issues + let program = module.program.clone(); + let import_name = import.name.clone(); + let module_path = import.module_path.clone(); + + // Evaluate the module to get the value + let mut module_interp = Interpreter::new(); + module_interp.run_with_modules(&program, loader)?; + + if let Some(value) = module_interp.global_env.get(&import_name) { + self.global_env.define(&name, value); + } else { + return Err(RuntimeError { + message: format!( + "'{}' not found in module '{}'", + import_name, module_path + ), + span: None, + }); + } + } + } + } + + Ok(()) + } + + /// Evaluate a declaration + fn eval_declaration(&mut self, decl: &Declaration) -> Result { + match decl { + Declaration::Function(func) => { + let closure = Closure { + params: func.params.iter().map(|p| p.name.name.clone()).collect(), + body: func.body.clone(), + env: self.global_env.clone(), + }; + let value = Value::Function(Rc::new(closure)); + self.global_env.define(&func.name.name, value.clone()); + Ok(value) + } + + Declaration::Let(let_decl) => { + let value = self.eval_expr(&let_decl.value, &self.global_env.clone())?; + self.global_env.define(&let_decl.name.name, value.clone()); + Ok(value) + } + + Declaration::Handler(handler) => { + let mut implementations = HashMap::new(); + for impl_ in &handler.implementations { + implementations.insert(impl_.op_name.name.clone(), impl_.clone()); + } + + let handler_value = HandlerValue { + effect: handler.effect.name.clone(), + implementations, + env: self.global_env.clone(), + }; + + let value = Value::Handler(Rc::new(handler_value)); + self.global_env.define(&handler.name.name, value.clone()); + Ok(value) + } + + Declaration::Effect(_) | Declaration::Type(_) => { + // These are compile-time only + Ok(Value::Unit) + } + } + } + + /// Evaluate an expression + fn eval_expr(&mut self, expr: &Expr, env: &Env) -> Result { + match self.eval_expr_inner(expr, env)? { + EvalResult::Value(v) => Ok(v), + EvalResult::Effect(req) => { + // Handle the effect + self.handle_effect(req) + } + } + } + + fn eval_expr_inner(&mut self, expr: &Expr, env: &Env) -> Result { + match expr { + Expr::Literal(lit) => Ok(EvalResult::Value(self.eval_literal(lit))), + + Expr::Var(ident) => match env.get(&ident.name) { + Some(value) => Ok(EvalResult::Value(value)), + None => Err(RuntimeError { + message: format!("Undefined variable: {}", ident.name), + span: Some(ident.span), + }), + }, + + Expr::BinaryOp { + op, + left, + right, + span, + } => { + let left_val = self.eval_expr(left, env)?; + let right_val = self.eval_expr(right, env)?; + Ok(EvalResult::Value( + self.eval_binary_op(*op, left_val, right_val, *span)?, + )) + } + + Expr::UnaryOp { op, operand, span } => { + let val = self.eval_expr(operand, env)?; + Ok(EvalResult::Value(self.eval_unary_op(*op, val, *span)?)) + } + + Expr::Call { func, args, span } => { + let func_val = self.eval_expr(func, env)?; + let arg_vals: Vec = args + .iter() + .map(|a| self.eval_expr(a, env)) + .collect::>()?; + + self.eval_call(func_val, arg_vals, *span) + } + + Expr::EffectOp { + effect, + operation, + args, + span, + } => { + // Check if this is a module call instead of an effect operation + // This includes stdlib modules (List, String, etc.) and user-imported modules + if let Some(module_val) = env.get(&effect.name) { + if let Value::Record(fields) = module_val { + if let Some(func) = fields.get(&operation.name) { + let arg_vals: Vec = args + .iter() + .map(|a| self.eval_expr(a, env)) + .collect::>()?; + return self.eval_call(func.clone(), arg_vals, *span); + } else { + return Err(RuntimeError { + message: format!( + "Module '{}' has no member '{}'", + effect.name, operation.name + ), + span: Some(*span), + }); + } + } + } + + let arg_vals: Vec = args + .iter() + .map(|a| self.eval_expr(a, env)) + .collect::>()?; + + // Create effect request + let request = EffectRequest { + effect: effect.name.clone(), + operation: operation.name.clone(), + args: arg_vals, + continuation: Continuation::new(), + }; + + Ok(EvalResult::Effect(request)) + } + + Expr::Field { + object, + field, + span, + } => { + let obj_val = self.eval_expr(object, env)?; + match obj_val { + Value::Record(fields) => match fields.get(&field.name) { + Some(v) => Ok(EvalResult::Value(v.clone())), + None => Err(RuntimeError { + message: format!("Record has no field '{}'", field.name), + span: Some(*span), + }), + }, + _ => Err(RuntimeError { + message: format!("Cannot access field on {}", obj_val.type_name()), + span: Some(*span), + }), + } + } + + Expr::Lambda { params, body, .. } => { + let closure = Closure { + params: params.iter().map(|p| p.name.name.clone()).collect(), + body: (**body).clone(), + env: env.clone(), + }; + Ok(EvalResult::Value(Value::Function(Rc::new(closure)))) + } + + Expr::Let { + name, value, body, .. + } => { + let val = self.eval_expr(value, env)?; + let new_env = env.extend(); + new_env.define(&name.name, val); + self.eval_expr_inner(body, &new_env) + } + + Expr::If { + condition, + then_branch, + else_branch, + span, + } => { + let cond_val = self.eval_expr(condition, env)?; + match cond_val { + Value::Bool(true) => self.eval_expr_inner(then_branch, env), + Value::Bool(false) => self.eval_expr_inner(else_branch, env), + _ => Err(RuntimeError { + message: format!("If condition must be Bool, got {}", cond_val.type_name()), + span: Some(*span), + }), + } + } + + Expr::Match { + scrutinee, + arms, + span, + } => { + let val = self.eval_expr(scrutinee, env)?; + self.eval_match(val, arms, env, *span) + } + + Expr::Block { + statements, result, .. + } => { + let block_env = env.extend(); + for stmt in statements { + match stmt { + Statement::Expr(e) => { + self.eval_expr(e, &block_env)?; + } + Statement::Let { name, value, .. } => { + let val = self.eval_expr(value, &block_env)?; + block_env.define(&name.name, val); + } + } + } + self.eval_expr_inner(result, &block_env) + } + + Expr::Record { fields, .. } => { + let mut record = HashMap::new(); + for (name, expr) in fields { + let val = self.eval_expr(expr, env)?; + record.insert(name.name.clone(), val); + } + Ok(EvalResult::Value(Value::Record(record))) + } + + Expr::Tuple { elements, .. } => { + let vals: Vec = elements + .iter() + .map(|e| self.eval_expr(e, env)) + .collect::>()?; + Ok(EvalResult::Value(Value::Tuple(vals))) + } + + Expr::List { elements, .. } => { + let vals: Vec = elements + .iter() + .map(|e| self.eval_expr(e, env)) + .collect::>()?; + Ok(EvalResult::Value(Value::List(vals))) + } + + Expr::Run { + expr, + handlers, + span, + } => self.eval_run(expr, handlers, env, *span), + + Expr::Resume { value, span } => Err(RuntimeError { + message: "Resume called outside of handler".to_string(), + span: Some(*span), + }), + } + } + + fn eval_literal(&self, lit: &Literal) -> Value { + match &lit.kind { + LiteralKind::Int(n) => Value::Int(*n), + LiteralKind::Float(f) => Value::Float(*f), + LiteralKind::String(s) => Value::String(s.clone()), + LiteralKind::Char(c) => Value::Char(*c), + LiteralKind::Bool(b) => Value::Bool(*b), + LiteralKind::Unit => Value::Unit, + } + } + + fn eval_binary_op( + &mut self, + op: BinaryOp, + left: Value, + right: Value, + span: Span, + ) -> Result { + match op { + BinaryOp::Add => match (left, right) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a + b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)), + (Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot add {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Sub => match (left, right) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a - b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot subtract {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Mul => match (left, right) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a * b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot multiply {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Div => match (left, right) { + (Value::Int(a), Value::Int(b)) => { + if b == 0 { + Err(RuntimeError { + message: "Division by zero".to_string(), + span: Some(span), + }) + } else { + Ok(Value::Int(a / b)) + } + } + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a / b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot divide {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Mod => match (left, right) { + (Value::Int(a), Value::Int(b)) => { + if b == 0 { + Err(RuntimeError { + message: "Modulo by zero".to_string(), + span: Some(span), + }) + } else { + Ok(Value::Int(a % b)) + } + } + (l, r) => Err(RuntimeError { + message: format!("Cannot modulo {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Eq => Ok(Value::Bool(self.values_equal(&left, &right))), + BinaryOp::Ne => Ok(Value::Bool(!self.values_equal(&left, &right))), + BinaryOp::Lt => match (left, right) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a < b)), + (Value::String(a), Value::String(b)) => Ok(Value::Bool(a < b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Le => match (left, right) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a <= b)), + (Value::String(a), Value::String(b)) => Ok(Value::Bool(a <= b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Gt => match (left, right) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a > b)), + (Value::String(a), Value::String(b)) => Ok(Value::Bool(a > b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Ge => match (left, right) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a >= b)), + (Value::String(a), Value::String(b)) => Ok(Value::Bool(a >= b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::And => match (left, right) { + (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a && b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot 'and' {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Or => match (left, right) { + (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a || b)), + (l, r) => Err(RuntimeError { + message: format!("Cannot 'or' {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, + BinaryOp::Pipe => { + // a |> f means f(a) + self.eval_call(right, vec![left], span) + .and_then(|r| match r { + EvalResult::Value(v) => Ok(v), + EvalResult::Effect(_) => Err(RuntimeError { + message: "Effect in pipe expression".to_string(), + span: Some(span), + }), + }) + } + } + } + + fn eval_unary_op(&self, op: UnaryOp, val: Value, span: Span) -> Result { + match op { + UnaryOp::Neg => match val { + Value::Int(n) => Ok(Value::Int(-n)), + Value::Float(f) => Ok(Value::Float(-f)), + v => Err(RuntimeError { + message: format!("Cannot negate {}", v.type_name()), + span: Some(span), + }), + }, + UnaryOp::Not => match val { + Value::Bool(b) => Ok(Value::Bool(!b)), + v => Err(RuntimeError { + message: format!("Cannot negate {}", v.type_name()), + span: Some(span), + }), + }, + } + } + + fn eval_call( + &mut self, + func: Value, + args: Vec, + span: Span, + ) -> Result { + match func { + Value::Function(closure) => { + if closure.params.len() != args.len() { + return Err(RuntimeError { + message: format!( + "Function expects {} arguments, got {}", + closure.params.len(), + args.len() + ), + span: Some(span), + }); + } + + let call_env = closure.env.extend(); + for (param, arg) in closure.params.iter().zip(args) { + call_env.define(param, arg); + } + + self.eval_expr_inner(&closure.body, &call_env) + } + Value::Constructor { name, fields } => { + // Constructor application + let mut new_fields = fields; + new_fields.extend(args); + Ok(EvalResult::Value(Value::Constructor { + name, + fields: new_fields, + })) + } + Value::Builtin(builtin) => self.eval_builtin(builtin, args, span), + v => Err(RuntimeError { + message: format!("Cannot call {}", v.type_name()), + span: Some(span), + }), + } + } + + fn eval_builtin( + &mut self, + builtin: BuiltinFn, + args: Vec, + span: Span, + ) -> Result { + let err = |msg: &str| RuntimeError { + message: msg.to_string(), + span: Some(span), + }; + + match builtin { + // List operations + BuiltinFn::ListMap => { + let (list, func) = + Self::expect_args_2::, Value>(&args, "List.map", span)?; + let mut result = Vec::with_capacity(list.len()); + for item in list { + match self.eval_call(func.clone(), vec![item], span)? { + EvalResult::Value(v) => result.push(v), + EvalResult::Effect(_) => return Err(err("Effect in List.map callback")), + } + } + Ok(EvalResult::Value(Value::List(result))) + } + + BuiltinFn::ListFilter => { + let (list, func) = + Self::expect_args_2::, Value>(&args, "List.filter", span)?; + let mut result = Vec::new(); + for item in list { + match self.eval_call(func.clone(), vec![item.clone()], span)? { + EvalResult::Value(Value::Bool(true)) => result.push(item), + EvalResult::Value(Value::Bool(false)) => {} + EvalResult::Value(v) => { + return Err(err(&format!( + "List.filter predicate must return Bool, got {}", + v.type_name() + ))) + } + EvalResult::Effect(_) => return Err(err("Effect in List.filter callback")), + } + } + Ok(EvalResult::Value(Value::List(result))) + } + + BuiltinFn::ListFold => { + // List.fold(list, initial, fn(acc, item) => ...) + if args.len() != 3 { + return Err(err( + "List.fold requires 3 arguments: list, initial, reducer", + )); + } + let list = match &args[0] { + Value::List(l) => l.clone(), + v => { + return Err(err(&format!( + "List.fold expects List as first argument, got {}", + v.type_name() + ))) + } + }; + let mut acc = args[1].clone(); + let func = args[2].clone(); + + for item in list { + match self.eval_call(func.clone(), vec![acc, item], span)? { + EvalResult::Value(v) => acc = v, + EvalResult::Effect(_) => return Err(err("Effect in List.fold callback")), + } + } + Ok(EvalResult::Value(acc)) + } + + BuiltinFn::ListHead => { + let list = Self::expect_arg_1::>(&args, "List.head", span)?; + match list.first() { + Some(v) => Ok(EvalResult::Value(Value::Constructor { + name: "Some".to_string(), + fields: vec![v.clone()], + })), + None => Ok(EvalResult::Value(Value::Constructor { + name: "None".to_string(), + fields: vec![], + })), + } + } + + BuiltinFn::ListTail => { + let list = Self::expect_arg_1::>(&args, "List.tail", span)?; + if list.is_empty() { + Ok(EvalResult::Value(Value::Constructor { + name: "None".to_string(), + fields: vec![], + })) + } else { + Ok(EvalResult::Value(Value::Constructor { + name: "Some".to_string(), + fields: vec![Value::List(list[1..].to_vec())], + })) + } + } + + BuiltinFn::ListConcat => { + let (list1, list2) = + Self::expect_args_2::, Vec>(&args, "List.concat", span)?; + let mut result = list1; + result.extend(list2); + Ok(EvalResult::Value(Value::List(result))) + } + + BuiltinFn::ListReverse => { + let mut list = Self::expect_arg_1::>(&args, "List.reverse", span)?; + list.reverse(); + Ok(EvalResult::Value(Value::List(list))) + } + + BuiltinFn::ListLength => { + let list = Self::expect_arg_1::>(&args, "List.length", span)?; + Ok(EvalResult::Value(Value::Int(list.len() as i64))) + } + + BuiltinFn::ListGet => { + let (list, idx) = Self::expect_args_2::, i64>(&args, "List.get", span)?; + if idx < 0 || idx as usize >= list.len() { + Ok(EvalResult::Value(Value::Constructor { + name: "None".to_string(), + fields: vec![], + })) + } else { + Ok(EvalResult::Value(Value::Constructor { + name: "Some".to_string(), + fields: vec![list[idx as usize].clone()], + })) + } + } + + BuiltinFn::ListRange => { + let (start, end) = Self::expect_args_2::(&args, "List.range", span)?; + let list: Vec = (start..end).map(Value::Int).collect(); + Ok(EvalResult::Value(Value::List(list))) + } + + // String operations + BuiltinFn::StringSplit => { + let (s, delim) = + Self::expect_args_2::(&args, "String.split", span)?; + let parts: Vec = s + .split(&delim) + .map(|p| Value::String(p.to_string())) + .collect(); + Ok(EvalResult::Value(Value::List(parts))) + } + + BuiltinFn::StringJoin => { + let (list, sep) = + Self::expect_args_2::, String>(&args, "String.join", span)?; + let strings: Result, _> = list + .iter() + .map(|v| match v { + Value::String(s) => Ok(s.clone()), + _ => Err(err("String.join requires list of strings")), + }) + .collect(); + Ok(EvalResult::Value(Value::String(strings?.join(&sep)))) + } + + BuiltinFn::StringTrim => { + let s = Self::expect_arg_1::(&args, "String.trim", span)?; + Ok(EvalResult::Value(Value::String(s.trim().to_string()))) + } + + BuiltinFn::StringContains => { + let (s, needle) = + Self::expect_args_2::(&args, "String.contains", span)?; + Ok(EvalResult::Value(Value::Bool(s.contains(&needle)))) + } + + BuiltinFn::StringReplace => { + if args.len() != 3 { + return Err(err("String.replace requires 3 arguments: string, from, to")); + } + let s = match &args[0] { + Value::String(s) => s.clone(), + v => { + return Err(err(&format!( + "String.replace expects String, got {}", + v.type_name() + ))) + } + }; + let from = match &args[1] { + Value::String(s) => s.clone(), + v => { + return Err(err(&format!( + "String.replace expects String, got {}", + v.type_name() + ))) + } + }; + let to = match &args[2] { + Value::String(s) => s.clone(), + v => { + return Err(err(&format!( + "String.replace expects String, got {}", + v.type_name() + ))) + } + }; + Ok(EvalResult::Value(Value::String(s.replace(&from, &to)))) + } + + BuiltinFn::StringLength => { + let s = Self::expect_arg_1::(&args, "String.length", span)?; + Ok(EvalResult::Value(Value::Int(s.len() as i64))) + } + + BuiltinFn::StringChars => { + let s = Self::expect_arg_1::(&args, "String.chars", span)?; + let chars: Vec = s.chars().map(Value::Char).collect(); + Ok(EvalResult::Value(Value::List(chars))) + } + + BuiltinFn::StringLines => { + let s = Self::expect_arg_1::(&args, "String.lines", span)?; + let lines: Vec = s.lines().map(|l| Value::String(l.to_string())).collect(); + Ok(EvalResult::Value(Value::List(lines))) + } + + // Option operations + BuiltinFn::OptionMap => { + let (opt, func) = Self::expect_args_2::(&args, "Option.map", span)?; + match opt { + Value::Constructor { name, fields } if name == "Some" && !fields.is_empty() => { + match self.eval_call(func, vec![fields[0].clone()], span)? { + EvalResult::Value(v) => Ok(EvalResult::Value(Value::Constructor { + name: "Some".to_string(), + fields: vec![v], + })), + EvalResult::Effect(_) => Err(err("Effect in Option.map callback")), + } + } + Value::Constructor { name, .. } if name == "None" => { + Ok(EvalResult::Value(Value::Constructor { + name: "None".to_string(), + fields: vec![], + })) + } + v => Err(err(&format!( + "Option.map expects Option, got {}", + v.type_name() + ))), + } + } + + BuiltinFn::OptionFlatMap => { + let (opt, func) = + Self::expect_args_2::(&args, "Option.flatMap", span)?; + match opt { + Value::Constructor { name, fields } if name == "Some" && !fields.is_empty() => { + match self.eval_call(func, vec![fields[0].clone()], span)? { + EvalResult::Value(v) => Ok(EvalResult::Value(v)), + EvalResult::Effect(_) => Err(err("Effect in Option.flatMap callback")), + } + } + Value::Constructor { name, .. } if name == "None" => { + Ok(EvalResult::Value(Value::Constructor { + name: "None".to_string(), + fields: vec![], + })) + } + v => Err(err(&format!( + "Option.flatMap expects Option, got {}", + v.type_name() + ))), + } + } + + BuiltinFn::OptionGetOrElse => { + let (opt, default) = + Self::expect_args_2::(&args, "Option.getOrElse", span)?; + match opt { + Value::Constructor { name, fields } if name == "Some" && !fields.is_empty() => { + Ok(EvalResult::Value(fields[0].clone())) + } + Value::Constructor { name, .. } if name == "None" => { + Ok(EvalResult::Value(default)) + } + v => Err(err(&format!( + "Option.getOrElse expects Option, got {}", + v.type_name() + ))), + } + } + + BuiltinFn::OptionIsSome => { + let opt = Self::expect_arg_1::(&args, "Option.isSome", span)?; + match opt { + Value::Constructor { name, .. } if name == "Some" => { + Ok(EvalResult::Value(Value::Bool(true))) + } + Value::Constructor { name, .. } if name == "None" => { + Ok(EvalResult::Value(Value::Bool(false))) + } + v => Err(err(&format!( + "Option.isSome expects Option, got {}", + v.type_name() + ))), + } + } + + BuiltinFn::OptionIsNone => { + let opt = Self::expect_arg_1::(&args, "Option.isNone", span)?; + match opt { + Value::Constructor { name, .. } if name == "None" => { + Ok(EvalResult::Value(Value::Bool(true))) + } + Value::Constructor { name, .. } if name == "Some" => { + Ok(EvalResult::Value(Value::Bool(false))) + } + v => Err(err(&format!( + "Option.isNone expects Option, got {}", + v.type_name() + ))), + } + } + + // Result operations + BuiltinFn::ResultMap => { + let (res, func) = Self::expect_args_2::(&args, "Result.map", span)?; + match res { + Value::Constructor { name, fields } if name == "Ok" && !fields.is_empty() => { + match self.eval_call(func, vec![fields[0].clone()], span)? { + EvalResult::Value(v) => Ok(EvalResult::Value(Value::Constructor { + name: "Ok".to_string(), + fields: vec![v], + })), + EvalResult::Effect(_) => Err(err("Effect in Result.map callback")), + } + } + Value::Constructor { name, fields } if name == "Err" => { + Ok(EvalResult::Value(Value::Constructor { + name: "Err".to_string(), + fields, + })) + } + v => Err(err(&format!( + "Result.map expects Result, got {}", + v.type_name() + ))), + } + } + + BuiltinFn::ResultFlatMap => { + let (res, func) = + Self::expect_args_2::(&args, "Result.flatMap", span)?; + match res { + Value::Constructor { name, fields } if name == "Ok" && !fields.is_empty() => { + match self.eval_call(func, vec![fields[0].clone()], span)? { + EvalResult::Value(v) => Ok(EvalResult::Value(v)), + EvalResult::Effect(_) => Err(err("Effect in Result.flatMap callback")), + } + } + Value::Constructor { name, fields } if name == "Err" => { + Ok(EvalResult::Value(Value::Constructor { + name: "Err".to_string(), + fields, + })) + } + v => Err(err(&format!( + "Result.flatMap expects Result, got {}", + v.type_name() + ))), + } + } + + BuiltinFn::ResultGetOrElse => { + let (res, default) = + Self::expect_args_2::(&args, "Result.getOrElse", span)?; + match res { + Value::Constructor { name, fields } if name == "Ok" && !fields.is_empty() => { + Ok(EvalResult::Value(fields[0].clone())) + } + Value::Constructor { name, .. } if name == "Err" => { + Ok(EvalResult::Value(default)) + } + v => Err(err(&format!( + "Result.getOrElse expects Result, got {}", + v.type_name() + ))), + } + } + + BuiltinFn::ResultIsOk => { + let res = Self::expect_arg_1::(&args, "Result.isOk", span)?; + match res { + Value::Constructor { name, .. } if name == "Ok" => { + Ok(EvalResult::Value(Value::Bool(true))) + } + Value::Constructor { name, .. } if name == "Err" => { + Ok(EvalResult::Value(Value::Bool(false))) + } + v => Err(err(&format!( + "Result.isOk expects Result, got {}", + v.type_name() + ))), + } + } + + BuiltinFn::ResultIsErr => { + let res = Self::expect_arg_1::(&args, "Result.isErr", span)?; + match res { + Value::Constructor { name, .. } if name == "Err" => { + Ok(EvalResult::Value(Value::Bool(true))) + } + Value::Constructor { name, .. } if name == "Ok" => { + Ok(EvalResult::Value(Value::Bool(false))) + } + v => Err(err(&format!( + "Result.isErr expects Result, got {}", + v.type_name() + ))), + } + } + + // Utility functions + BuiltinFn::Print => { + for arg in &args { + match arg { + Value::String(s) => print!("{}", s), + v => print!("{}", v), + } + } + println!(); + Ok(EvalResult::Value(Value::Unit)) + } + + BuiltinFn::ToString => { + if args.len() != 1 { + return Err(err("toString requires 1 argument")); + } + Ok(EvalResult::Value(Value::String(format!("{}", args[0])))) + } + + BuiltinFn::TypeOf => { + if args.len() != 1 { + return Err(err("typeOf requires 1 argument")); + } + Ok(EvalResult::Value(Value::String( + args[0].type_name().to_string(), + ))) + } + } + } + + // Helper functions for extracting typed arguments + fn expect_arg_1(args: &[Value], name: &str, span: Span) -> Result + where + T: TryFromValue, + { + if args.len() != 1 { + return Err(RuntimeError { + message: format!("{} requires 1 argument, got {}", name, args.len()), + span: Some(span), + }); + } + T::try_from_value(&args[0]).ok_or_else(|| RuntimeError { + message: format!("{} expects {} as argument", name, T::TYPE_NAME), + span: Some(span), + }) + } + + fn expect_args_2(args: &[Value], name: &str, span: Span) -> Result<(T, U), RuntimeError> + where + T: TryFromValue, + U: TryFromValue, + { + if args.len() != 2 { + return Err(RuntimeError { + message: format!("{} requires 2 arguments, got {}", name, args.len()), + span: Some(span), + }); + } + let a = T::try_from_value(&args[0]).ok_or_else(|| RuntimeError { + message: format!("{} expects {} as first argument", name, T::TYPE_NAME), + span: Some(span), + })?; + let b = U::try_from_value(&args[1]).ok_or_else(|| RuntimeError { + message: format!("{} expects {} as second argument", name, U::TYPE_NAME), + span: Some(span), + })?; + Ok((a, b)) + } + + fn eval_match( + &mut self, + val: Value, + arms: &[MatchArm], + env: &Env, + span: Span, + ) -> Result { + for arm in arms { + if let Some(bindings) = self.match_pattern(&arm.pattern, &val) { + let match_env = env.extend(); + for (name, value) in bindings { + match_env.define(name, value); + } + + // Check guard if present + if let Some(ref guard) = arm.guard { + let guard_val = self.eval_expr(guard, &match_env)?; + match guard_val { + Value::Bool(true) => {} + Value::Bool(false) => continue, + _ => { + return Err(RuntimeError { + message: "Match guard must be Bool".to_string(), + span: Some(arm.span), + }); + } + } + } + + return self.eval_expr_inner(&arm.body, &match_env); + } + } + + Err(RuntimeError { + message: "No matching pattern".to_string(), + span: Some(span), + }) + } + + fn match_pattern(&self, pattern: &Pattern, value: &Value) -> Option> { + match pattern { + Pattern::Wildcard(_) => Some(Vec::new()), + + Pattern::Var(ident) => Some(vec![(ident.name.clone(), value.clone())]), + + Pattern::Literal(lit) => { + let lit_val = self.eval_literal(lit); + if self.values_equal(&lit_val, value) { + Some(Vec::new()) + } else { + None + } + } + + Pattern::Constructor { name, fields, .. } => match value { + Value::Constructor { + name: val_name, + fields: val_fields, + } => { + if name.name != *val_name { + return None; + } + if fields.len() != val_fields.len() { + return None; + } + let mut bindings = Vec::new(); + for (pat, val) in fields.iter().zip(val_fields) { + bindings.extend(self.match_pattern(pat, val)?); + } + Some(bindings) + } + _ => None, + }, + + Pattern::Tuple { elements, .. } => match value { + Value::Tuple(vals) => { + if elements.len() != vals.len() { + return None; + } + let mut bindings = Vec::new(); + for (pat, val) in elements.iter().zip(vals) { + bindings.extend(self.match_pattern(pat, val)?); + } + Some(bindings) + } + _ => None, + }, + + Pattern::Record { fields, .. } => match value { + Value::Record(val_fields) => { + let mut bindings = Vec::new(); + for (name, pat) in fields { + let val = val_fields.get(&name.name)?; + bindings.extend(self.match_pattern(pat, val)?); + } + Some(bindings) + } + _ => None, + }, + } + } + + fn values_equal(&self, a: &Value, b: &Value) -> bool { + match (a, b) { + (Value::Int(a), Value::Int(b)) => a == b, + (Value::Float(a), Value::Float(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::String(a), Value::String(b)) => a == b, + (Value::Char(a), Value::Char(b)) => a == b, + (Value::Unit, Value::Unit) => true, + (Value::List(a), Value::List(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(x, y)| self.values_equal(x, y)) + } + (Value::Tuple(a), Value::Tuple(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(x, y)| self.values_equal(x, y)) + } + ( + Value::Constructor { + name: n1, + fields: f1, + }, + Value::Constructor { + name: n2, + fields: f2, + }, + ) => { + n1 == n2 + && f1.len() == f2.len() + && f1.iter().zip(f2).all(|(x, y)| self.values_equal(x, y)) + } + _ => false, + } + } + + fn eval_run( + &mut self, + expr: &Expr, + handlers: &[(Ident, Expr)], + env: &Env, + span: Span, + ) -> Result { + // Evaluate handlers and push onto stack + let mut handler_values = Vec::new(); + for (effect_name, handler_expr) in handlers { + let handler_val = self.eval_expr(handler_expr, env)?; + match handler_val { + Value::Handler(h) => { + if h.effect != effect_name.name { + return Err(RuntimeError { + message: format!( + "Handler for effect '{}' assigned to '{}'", + h.effect, effect_name.name + ), + span: Some(span), + }); + } + handler_values.push(h); + } + _ => { + return Err(RuntimeError { + message: format!( + "Expected handler for effect '{}', got {}", + effect_name.name, + handler_val.type_name() + ), + span: Some(span), + }); + } + } + } + + // Push handlers + for h in &handler_values { + self.handler_stack.push(Rc::clone(h)); + } + + // Evaluate expression + let result = self.eval_expr_inner(expr, env); + + // Pop handlers + for _ in &handler_values { + self.handler_stack.pop(); + } + + result + } + + fn handle_effect(&mut self, request: EffectRequest) -> Result { + let trace_enabled = self.trace_effects; + let timestamp = if trace_enabled { + self.start_time.elapsed().as_micros() + } else { + 0 + }; + + // Find a handler for this effect - clone what we need to avoid borrow issues + let handler_data: Option<(Env, crate::ast::Expr, Vec)> = self + .handler_stack + .iter() + .rev() + .find(|h| h.effect == request.effect) + .and_then(|handler| { + handler + .implementations + .get(&request.operation) + .map(|impl_| { + ( + handler.env.clone(), + impl_.body.clone(), + impl_.params.clone(), + ) + }) + }); + + let result = if let Some((handler_env, body, params)) = handler_data { + let env = handler_env.extend(); + for (i, param) in params.iter().enumerate() { + if i < request.args.len() { + env.define(¶m.name, request.args[i].clone()); + } + } + self.eval_expr(&body, &env) + } else { + // No handler found - check for built-in effects + self.handle_builtin_effect(&request) + }; + + // Record trace if enabled + if trace_enabled { + self.effect_traces.push(EffectTrace { + effect: request.effect.clone(), + operation: request.operation.clone(), + args: request.args.clone(), + result: result.as_ref().ok().cloned(), + timestamp_us: timestamp, + }); + } + + result + } + + fn handle_builtin_effect(&self, request: &EffectRequest) -> Result { + match (request.effect.as_str(), request.operation.as_str()) { + ("Console", "print") => { + if let Some(Value::String(s)) = request.args.first() { + println!("{}", s); + Ok(Value::Unit) + } else if let Some(v) = request.args.first() { + println!("{}", v); + Ok(Value::Unit) + } else { + Ok(Value::Unit) + } + } + ("Console", "read") => { + let mut input = String::new(); + std::io::stdin() + .read_line(&mut input) + .map_err(|e| RuntimeError { + message: format!("Failed to read input: {}", e), + span: None, + })?; + Ok(Value::String(input.trim().to_string())) + } + ("Fail", "fail") => { + let msg = request + .args + .first() + .map(|v| format!("{}", v)) + .unwrap_or_else(|| "Unknown error".to_string()); + Err(RuntimeError { + message: msg, + span: None, + }) + } + _ => Err(RuntimeError { + message: format!( + "Unhandled effect operation: {}.{}", + request.effect, request.operation + ), + span: None, + }), + } + } +} + +impl Default for Interpreter { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_versioned() { + let interp = Interpreter::new(); + let record = Value::Record( + [("name".to_string(), Value::String("Alice".to_string()))] + .into_iter() + .collect(), + ); + + let versioned = interp.create_versioned("User", 1, record); + + match versioned { + Value::Versioned { + type_name, + version, + value, + } => { + assert_eq!(type_name, "User"); + assert_eq!(version, 1); + match *value { + Value::Record(fields) => match fields.get("name") { + Some(Value::String(s)) => assert_eq!(s, "Alice"), + _ => panic!("Expected name field with String value"), + }, + _ => panic!("Expected Record value"), + } + } + _ => panic!("Expected Versioned value"), + } + } + + #[test] + fn test_migrate_non_versioned_passthrough() { + let mut interp = Interpreter::new(); + let value = Value::Int(42); + + let result = interp.migrate_value(value, 2).unwrap(); + + match result { + Value::Int(n) => assert_eq!(n, 42), + _ => panic!("Expected Int value to pass through unchanged"), + } + } + + #[test] + fn test_migrate_same_version() { + let mut interp = Interpreter::new(); + let versioned = Value::Versioned { + type_name: "User".to_string(), + version: 2, + value: Box::new(Value::String("test".to_string())), + }; + + let result = interp.migrate_value(versioned, 2).unwrap(); + + match result { + Value::Versioned { version, .. } => assert_eq!(version, 2), + _ => panic!("Expected Versioned value"), + } + } + + #[test] + fn test_migrate_downgrade_error() { + let mut interp = Interpreter::new(); + let versioned = Value::Versioned { + type_name: "User".to_string(), + version: 3, + value: Box::new(Value::String("test".to_string())), + }; + + let result = interp.migrate_value(versioned, 2); + + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("Cannot downgrade")); + } + + #[test] + fn test_migrate_with_auto_migration() { + let mut interp = Interpreter::new(); + // No explicit migration registered - should auto-migrate + let versioned = Value::Versioned { + type_name: "User".to_string(), + version: 1, + value: Box::new(Value::String("test".to_string())), + }; + + let result = interp.migrate_value(versioned, 3).unwrap(); + + match result { + Value::Versioned { version, value, .. } => { + assert_eq!(version, 3); + // Value should be unchanged for auto-migration + match *value { + Value::String(s) => assert_eq!(s, "test"), + _ => panic!("Expected String value"), + } + } + _ => panic!("Expected Versioned value"), + } + } + + #[test] + fn test_register_and_execute_migration() { + let mut interp = Interpreter::new(); + + // Create a simple migration that adds a field + // Migration: old.name -> { name: old.name, email: "unknown" } + let migration_body = Expr::Record { + fields: vec![ + ( + Ident::new("name", Span::default()), + Expr::Field { + object: Box::new(Expr::Var(Ident::new("old", Span::default()))), + field: Ident::new("name", Span::default()), + span: Span::default(), + }, + ), + ( + Ident::new("email", Span::default()), + Expr::Literal(Literal { + kind: LiteralKind::String("unknown@example.com".to_string()), + span: Span::default(), + }), + ), + ], + span: Span::default(), + }; + + let stored_migration = StoredMigration { + body: migration_body, + env: Env::new(), + }; + + interp.register_migration("User", 1, stored_migration); + + // Create a v1 value + let v1_user = Value::Versioned { + type_name: "User".to_string(), + version: 1, + value: Box::new(Value::Record( + [("name".to_string(), Value::String("Alice".to_string()))] + .into_iter() + .collect(), + )), + }; + + // Migrate to v2 + let result = interp.migrate_value(v1_user, 2).unwrap(); + + match result { + Value::Versioned { + type_name, + version, + value, + } => { + assert_eq!(type_name, "User"); + assert_eq!(version, 2); + match *value { + Value::Record(fields) => { + match fields.get("name") { + Some(Value::String(s)) => assert_eq!(s, "Alice"), + _ => panic!("Expected name field with String value"), + } + match fields.get("email") { + Some(Value::String(s)) => assert_eq!(s, "unknown@example.com"), + _ => panic!("Expected email field with String value"), + } + } + _ => panic!("Expected Record value"), + } + } + _ => panic!("Expected Versioned value"), + } + } +} diff --git a/src/lexer.rs b/src/lexer.rs new file mode 100644 index 0000000..1574c3d --- /dev/null +++ b/src/lexer.rs @@ -0,0 +1,633 @@ +//! Lexer for the Lux language + +#![allow(dead_code)] + +use crate::ast::Span; +use std::fmt; +use std::iter::Peekable; +use std::str::Chars; + +/// Token types +#[derive(Debug, Clone, PartialEq)] +pub enum TokenKind { + // Literals + Int(i64), + Float(f64), + String(String), + Char(char), + Bool(bool), + + // Identifiers and keywords + Ident(String), + + // Keywords + Fn, + Let, + If, + Then, + Else, + Match, + With, + Effect, + Handler, + Run, + Resume, + Type, + True, + False, + Import, + Pub, + As, + From, // from (for migrations) + Latest, // latest (for @latest version constraint) + + // Operators + Plus, // + + Minus, // - + Star, // * + Slash, // / + Percent, // % + Eq, // = + EqEq, // == + Ne, // != + Lt, // < + Le, // <= + Gt, // > + Ge, // >= + And, // && + Or, // || + Not, // ! + Pipe, // | + PipeGt, // |> + Arrow, // => + ThinArrow, // -> + Dot, // . + Colon, // : + ColonColon, // :: + Comma, // , + Semi, // ; + At, // @ + + // Delimiters + LParen, // ( + RParen, // ) + LBrace, // { + RBrace, // } + LBracket, // [ + RBracket, // ] + + // Special + Underscore, // _ + Newline, + Eof, +} + +impl fmt::Display for TokenKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TokenKind::Int(n) => write!(f, "{}", n), + TokenKind::Float(n) => write!(f, "{}", n), + TokenKind::String(s) => write!(f, "\"{}\"", s), + TokenKind::Char(c) => write!(f, "'{}'", c), + TokenKind::Bool(b) => write!(f, "{}", b), + TokenKind::Ident(s) => write!(f, "{}", s), + TokenKind::Fn => write!(f, "fn"), + TokenKind::Let => write!(f, "let"), + TokenKind::If => write!(f, "if"), + TokenKind::Then => write!(f, "then"), + TokenKind::Else => write!(f, "else"), + TokenKind::Match => write!(f, "match"), + TokenKind::With => write!(f, "with"), + TokenKind::Effect => write!(f, "effect"), + TokenKind::Handler => write!(f, "handler"), + TokenKind::Run => write!(f, "run"), + TokenKind::Resume => write!(f, "resume"), + TokenKind::Type => write!(f, "type"), + TokenKind::Import => write!(f, "import"), + TokenKind::Pub => write!(f, "pub"), + TokenKind::As => write!(f, "as"), + TokenKind::From => write!(f, "from"), + TokenKind::Latest => write!(f, "latest"), + TokenKind::True => write!(f, "true"), + TokenKind::False => write!(f, "false"), + TokenKind::Plus => write!(f, "+"), + TokenKind::Minus => write!(f, "-"), + TokenKind::Star => write!(f, "*"), + TokenKind::Slash => write!(f, "/"), + TokenKind::Percent => write!(f, "%"), + TokenKind::Eq => write!(f, "="), + TokenKind::EqEq => write!(f, "=="), + TokenKind::Ne => write!(f, "!="), + TokenKind::Lt => write!(f, "<"), + TokenKind::Le => write!(f, "<="), + TokenKind::Gt => write!(f, ">"), + TokenKind::Ge => write!(f, ">="), + TokenKind::And => write!(f, "&&"), + TokenKind::Or => write!(f, "||"), + TokenKind::Not => write!(f, "!"), + TokenKind::Pipe => write!(f, "|"), + TokenKind::PipeGt => write!(f, "|>"), + TokenKind::Arrow => write!(f, "=>"), + TokenKind::ThinArrow => write!(f, "->"), + TokenKind::Dot => write!(f, "."), + TokenKind::Colon => write!(f, ":"), + TokenKind::ColonColon => write!(f, "::"), + TokenKind::Comma => write!(f, ","), + TokenKind::Semi => write!(f, ";"), + TokenKind::At => write!(f, "@"), + TokenKind::LParen => write!(f, "("), + TokenKind::RParen => write!(f, ")"), + TokenKind::LBrace => write!(f, "{{"), + TokenKind::RBrace => write!(f, "}}"), + TokenKind::LBracket => write!(f, "["), + TokenKind::RBracket => write!(f, "]"), + TokenKind::Underscore => write!(f, "_"), + TokenKind::Newline => write!(f, "\\n"), + TokenKind::Eof => write!(f, "EOF"), + } + } +} + +/// A token with its source location +#[derive(Debug, Clone)] +pub struct Token { + pub kind: TokenKind, + pub span: Span, +} + +impl Token { + pub fn new(kind: TokenKind, span: Span) -> Self { + Self { kind, span } + } +} + +/// Lexer error +#[derive(Debug, Clone)] +pub struct LexError { + pub message: String, + pub span: Span, +} + +impl fmt::Display for LexError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Lexer error at {}-{}: {}", + self.span.start, self.span.end, self.message + ) + } +} + +/// The lexer +pub struct Lexer<'a> { + source: &'a str, + chars: Peekable>, + pos: usize, +} + +impl<'a> Lexer<'a> { + pub fn new(source: &'a str) -> Self { + Self { + source, + chars: source.chars().peekable(), + pos: 0, + } + } + + /// Tokenize the entire source + pub fn tokenize(mut self) -> Result, LexError> { + let mut tokens = Vec::new(); + loop { + let token = self.next_token()?; + let is_eof = token.kind == TokenKind::Eof; + tokens.push(token); + if is_eof { + break; + } + } + Ok(tokens) + } + + fn next_token(&mut self) -> Result { + self.skip_whitespace_and_comments(); + + let start = self.pos; + + let Some(c) = self.advance() else { + return Ok(Token::new(TokenKind::Eof, Span::new(start, start))); + }; + + let kind = match c { + // Single-character tokens + '+' => TokenKind::Plus, + '*' => TokenKind::Star, + '%' => TokenKind::Percent, + '(' => TokenKind::LParen, + ')' => TokenKind::RParen, + '{' => TokenKind::LBrace, + '}' => TokenKind::RBrace, + '[' => TokenKind::LBracket, + ']' => TokenKind::RBracket, + ',' => TokenKind::Comma, + ';' => TokenKind::Semi, + '@' => TokenKind::At, + '\n' => TokenKind::Newline, + + // Multi-character tokens + '-' => { + if self.peek() == Some('>') { + self.advance(); + TokenKind::ThinArrow + } else { + TokenKind::Minus + } + } + '/' => { + if self.peek() == Some('/') { + // Line comment + self.skip_line_comment(); + return self.next_token(); + } else { + TokenKind::Slash + } + } + '=' => { + if self.peek() == Some('=') { + self.advance(); + TokenKind::EqEq + } else if self.peek() == Some('>') { + self.advance(); + TokenKind::Arrow + } else { + TokenKind::Eq + } + } + '!' => { + if self.peek() == Some('=') { + self.advance(); + TokenKind::Ne + } else { + TokenKind::Not + } + } + '<' => { + if self.peek() == Some('=') { + self.advance(); + TokenKind::Le + } else { + TokenKind::Lt + } + } + '>' => { + if self.peek() == Some('=') { + self.advance(); + TokenKind::Ge + } else { + TokenKind::Gt + } + } + '&' => { + if self.peek() == Some('&') { + self.advance(); + TokenKind::And + } else { + return Err(LexError { + message: "Expected '&&'".into(), + span: Span::new(start, self.pos), + }); + } + } + '|' => { + if self.peek() == Some('|') { + self.advance(); + TokenKind::Or + } else if self.peek() == Some('>') { + self.advance(); + TokenKind::PipeGt + } else { + TokenKind::Pipe + } + } + '.' => TokenKind::Dot, + ':' => { + if self.peek() == Some(':') { + self.advance(); + TokenKind::ColonColon + } else { + TokenKind::Colon + } + } + '_' => { + if self.peek().map_or(false, |c| c.is_alphanumeric()) { + // It's an identifier starting with _ + self.scan_ident_rest(start) + } else { + TokenKind::Underscore + } + } + + // String literals + '"' => self.scan_string(start)?, + + // Char literals + '\'' => self.scan_char(start)?, + + // Numbers + c if c.is_ascii_digit() => self.scan_number(c, start)?, + + // Identifiers and keywords + c if c.is_alphabetic() || c == '_' => self.scan_ident_rest(start), + + _ => { + return Err(LexError { + message: format!("Unexpected character: '{}'", c), + span: Span::new(start, self.pos), + }); + } + }; + + Ok(Token::new(kind, Span::new(start, self.pos))) + } + + fn advance(&mut self) -> Option { + let c = self.chars.next()?; + self.pos += c.len_utf8(); + Some(c) + } + + fn peek(&mut self) -> Option { + self.chars.peek().copied() + } + + fn skip_whitespace_and_comments(&mut self) { + while let Some(c) = self.peek() { + if c == ' ' || c == '\t' || c == '\r' { + self.advance(); + } else if c == '/' { + // Check for comment + let mut chars = self.chars.clone(); + chars.next(); // consume '/' + if chars.peek() == Some(&'/') { + self.skip_line_comment(); + } else { + break; + } + } else { + break; + } + } + } + + fn skip_line_comment(&mut self) { + while let Some(c) = self.peek() { + if c == '\n' { + break; + } + self.advance(); + } + } + + fn scan_string(&mut self, _start: usize) -> Result { + let mut value = String::new(); + loop { + match self.advance() { + Some('"') => break, + Some('\\') => { + let escaped = match self.advance() { + Some('n') => '\n', + Some('r') => '\r', + Some('t') => '\t', + Some('\\') => '\\', + Some('"') => '"', + Some(c) => c, + None => { + return Err(LexError { + message: "Unterminated string".into(), + span: Span::new(_start, self.pos), + }); + } + }; + value.push(escaped); + } + Some(c) => value.push(c), + None => { + return Err(LexError { + message: "Unterminated string".into(), + span: Span::new(_start, self.pos), + }); + } + } + } + Ok(TokenKind::String(value)) + } + + fn scan_char(&mut self, start: usize) -> Result { + let c = match self.advance() { + Some('\\') => match self.advance() { + Some('n') => '\n', + Some('r') => '\r', + Some('t') => '\t', + Some('\\') => '\\', + Some('\'') => '\'', + Some(c) => c, + None => { + return Err(LexError { + message: "Unterminated character literal".into(), + span: Span::new(start, self.pos), + }); + } + }, + Some(c) => c, + None => { + return Err(LexError { + message: "Unterminated character literal".into(), + span: Span::new(start, self.pos), + }); + } + }; + + if self.advance() != Some('\'') { + return Err(LexError { + message: "Expected closing quote for character literal".into(), + span: Span::new(start, self.pos), + }); + } + + Ok(TokenKind::Char(c)) + } + + fn scan_number(&mut self, first: char, start: usize) -> Result { + let mut num_str = String::new(); + num_str.push(first); + + while let Some(c) = self.peek() { + if c.is_ascii_digit() || c == '_' { + if c != '_' { + num_str.push(c); + } + self.advance(); + } else { + break; + } + } + + // Check for float + if self.peek() == Some('.') { + // Look ahead to make sure it's not a method call + let mut chars = self.chars.clone(); + chars.next(); // consume '.' + if chars.peek().map_or(false, |c| c.is_ascii_digit()) { + self.advance(); // consume '.' + num_str.push('.'); + while let Some(c) = self.peek() { + if c.is_ascii_digit() || c == '_' { + if c != '_' { + num_str.push(c); + } + self.advance(); + } else { + break; + } + } + let f: f64 = num_str.parse().map_err(|_| LexError { + message: "Invalid float literal".into(), + span: Span::new(start, self.pos), + })?; + return Ok(TokenKind::Float(f)); + } + } + + let n: i64 = num_str.parse().map_err(|_| LexError { + message: "Invalid integer literal".into(), + span: Span::new(start, self.pos), + })?; + Ok(TokenKind::Int(n)) + } + + fn scan_ident_rest(&mut self, start: usize) -> TokenKind { + while let Some(c) = self.peek() { + if c.is_alphanumeric() || c == '_' { + self.advance(); + } else { + break; + } + } + + let ident = &self.source[start..self.pos]; + match ident { + "fn" => TokenKind::Fn, + "let" => TokenKind::Let, + "if" => TokenKind::If, + "then" => TokenKind::Then, + "else" => TokenKind::Else, + "match" => TokenKind::Match, + "with" => TokenKind::With, + "effect" => TokenKind::Effect, + "handler" => TokenKind::Handler, + "run" => TokenKind::Run, + "resume" => TokenKind::Resume, + "type" => TokenKind::Type, + "import" => TokenKind::Import, + "pub" => TokenKind::Pub, + "as" => TokenKind::As, + "from" => TokenKind::From, + "latest" => TokenKind::Latest, + "true" => TokenKind::Bool(true), + "false" => TokenKind::Bool(false), + _ => TokenKind::Ident(ident.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lex(source: &str) -> Vec { + Lexer::new(source) + .tokenize() + .unwrap() + .into_iter() + .map(|t| t.kind) + .filter(|k| !matches!(k, TokenKind::Newline)) + .collect() + } + + #[test] + fn test_basic_tokens() { + assert_eq!( + lex("fn let if else"), + vec![ + TokenKind::Fn, + TokenKind::Let, + TokenKind::If, + TokenKind::Else, + TokenKind::Eof + ] + ); + } + + #[test] + fn test_operators() { + assert_eq!( + lex("+ - * / == != |>"), + vec![ + TokenKind::Plus, + TokenKind::Minus, + TokenKind::Star, + TokenKind::Slash, + TokenKind::EqEq, + TokenKind::Ne, + TokenKind::PipeGt, + TokenKind::Eof + ] + ); + } + + #[test] + fn test_numbers() { + assert_eq!( + lex("42 3.14"), + vec![TokenKind::Int(42), TokenKind::Float(3.14), TokenKind::Eof] + ); + } + + #[test] + fn test_strings() { + assert_eq!( + lex("\"hello\" \"world\""), + vec![ + TokenKind::String("hello".into()), + TokenKind::String("world".into()), + TokenKind::Eof + ] + ); + } + + #[test] + fn test_function() { + assert_eq!( + lex("fn add(a: Int, b: Int): Int = a + b"), + vec![ + TokenKind::Fn, + TokenKind::Ident("add".into()), + TokenKind::LParen, + TokenKind::Ident("a".into()), + TokenKind::Colon, + TokenKind::Ident("Int".into()), + TokenKind::Comma, + TokenKind::Ident("b".into()), + TokenKind::Colon, + TokenKind::Ident("Int".into()), + TokenKind::RParen, + TokenKind::Colon, + TokenKind::Ident("Int".into()), + TokenKind::Eq, + TokenKind::Ident("a".into()), + TokenKind::Plus, + TokenKind::Ident("b".into()), + TokenKind::Eof + ] + ); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..89167b7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,791 @@ +//! Lux - A functional programming language with first-class effects + +mod ast; +mod interpreter; +mod lexer; +mod modules; +mod parser; +mod schema; +mod typechecker; +mod types; + +use interpreter::Interpreter; +use parser::Parser; +use std::io::{self, Write}; +use typechecker::TypeChecker; + +const VERSION: &str = "0.1.0"; + +const HELP: &str = r#" +Lux - A functional language with first-class effects + +Commands: + :help, :h Show this help + :quit, :q Exit the REPL + :type Show the type of an expression + :clear Clear the environment + :load Load and execute a file + :trace on/off Enable/disable effect tracing + :traces Show recorded effect traces + +Examples: + > let x = 42 + > x + 1 + 43 + + > fn double(n: Int): Int = n * 2 + > double(21) + 42 + + > Console.print("Hello, world!") + Hello, world! + +Debugging: + > :trace on + > Console.print("test") + > :traces + [ 0.123ms] Console.print("test") → () +"#; + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.len() > 1 { + // Run a file + run_file(&args[1]); + } else { + // Start REPL + run_repl(); + } +} + +fn run_file(path: &str) { + use modules::ModuleLoader; + use std::path::Path; + + let file_path = Path::new(path); + let source = match std::fs::read_to_string(file_path) { + Ok(s) => s, + Err(e) => { + eprintln!("Error reading file '{}': {}", path, e); + std::process::exit(1); + } + }; + + // Set up module loader with the file's directory as a search path + let mut loader = ModuleLoader::new(); + if let Some(parent) = file_path.parent() { + loader.add_search_path(parent.to_path_buf()); + } + + // Load and parse the program (including any imports) + let program = match loader.load_source(&source, Some(file_path)) { + Ok(p) => p, + Err(e) => { + eprintln!("Module error: {}", e); + std::process::exit(1); + } + }; + + let mut checker = TypeChecker::new(); + if let Err(errors) = checker.check_program_with_modules(&program, &loader) { + for error in errors { + eprintln!("Type error: {}", error); + } + std::process::exit(1); + } + + let mut interp = Interpreter::new(); + match interp.run_with_modules(&program, &loader) { + Ok(value) => { + if !matches!(value, interpreter::Value::Unit) { + println!("{}", value); + } + } + Err(e) => { + eprintln!("Runtime error: {}", e); + std::process::exit(1); + } + } +} + +fn run_repl() { + println!("Lux v{}", VERSION); + println!("Type :help for help, :quit to exit\n"); + + let mut interp = Interpreter::new(); + let mut checker = TypeChecker::new(); + let mut buffer = String::new(); + let mut continuation = false; + + loop { + // Print prompt + let prompt = if continuation { "... " } else { "lux> " }; + print!("{}", prompt); + io::stdout().flush().unwrap(); + + // Read input + let mut line = String::new(); + match io::stdin().read_line(&mut line) { + Ok(0) => break, // EOF + Ok(_) => {} + Err(e) => { + eprintln!("Error reading input: {}", e); + continue; + } + } + + let line = line.trim_end(); + + // Handle commands + if !continuation && line.starts_with(':') { + handle_command(line, &mut interp, &mut checker); + continue; + } + + // Accumulate input + buffer.push_str(line); + buffer.push('\n'); + + // Check for continuation (simple heuristic: unbalanced braces) + let open_braces = buffer.chars().filter(|c| *c == '{').count(); + let close_braces = buffer.chars().filter(|c| *c == '}').count(); + let open_parens = buffer.chars().filter(|c| *c == '(').count(); + let close_parens = buffer.chars().filter(|c| *c == ')').count(); + + if open_braces > close_braces || open_parens > close_parens { + continuation = true; + continue; + } + + continuation = false; + let input = std::mem::take(&mut buffer); + + if input.trim().is_empty() { + continue; + } + + eval_input(&input, &mut interp, &mut checker); + } + + println!("\nGoodbye!"); +} + +fn handle_command(line: &str, interp: &mut Interpreter, checker: &mut TypeChecker) { + let parts: Vec<&str> = line.splitn(2, ' ').collect(); + let cmd = parts[0]; + let arg = parts.get(1).map(|s| s.trim()); + + match cmd { + ":help" | ":h" => { + println!("{}", HELP); + } + ":quit" | ":q" => { + println!("Goodbye!"); + std::process::exit(0); + } + ":type" | ":t" => { + if let Some(expr_str) = arg { + show_type(expr_str, checker); + } else { + println!("Usage: :type "); + } + } + ":clear" => { + *interp = Interpreter::new(); + *checker = TypeChecker::new(); + println!("Environment cleared."); + } + ":load" | ":l" => { + if let Some(path) = arg { + load_file(path, interp, checker); + } else { + println!("Usage: :load "); + } + } + ":trace" => match arg { + Some("on") => { + interp.enable_tracing(); + println!("Effect tracing enabled."); + } + Some("off") => { + interp.trace_effects = false; + println!("Effect tracing disabled."); + } + _ => { + println!("Usage: :trace on|off"); + } + }, + ":traces" => { + if interp.get_traces().is_empty() { + println!("No effect traces recorded. Use :trace on to enable tracing."); + } else { + interp.print_traces(); + } + } + _ => { + println!("Unknown command: {}", cmd); + println!("Type :help for help"); + } + } +} + +fn show_type(expr_str: &str, checker: &mut TypeChecker) { + // Wrap expression in a let to parse it + let wrapped = format!("let _expr_ = {}", expr_str); + + match Parser::parse_source(&wrapped) { + Ok(program) => { + if let Err(errors) = checker.check_program(&program) { + for error in errors { + println!("Type error: {}", error); + } + } else { + println!("(type checking passed)"); + } + } + Err(e) => { + println!("Parse error: {}", e); + } + } +} + +fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker) { + let source = match std::fs::read_to_string(path) { + Ok(s) => s, + Err(e) => { + println!("Error reading file '{}': {}", path, e); + return; + } + }; + + let program = match Parser::parse_source(&source) { + Ok(p) => p, + Err(e) => { + println!("Parse error: {}", e); + return; + } + }; + + if let Err(errors) = checker.check_program(&program) { + for error in errors { + println!("Type error: {}", error); + } + return; + } + + match interp.run(&program) { + Ok(_) => println!("Loaded '{}'", path), + Err(e) => println!("Runtime error: {}", e), + } +} + +fn eval_input(input: &str, interp: &mut Interpreter, checker: &mut TypeChecker) { + // Try to parse as a program (declarations) + match Parser::parse_source(input) { + Ok(program) => { + // Type check + if let Err(errors) = checker.check_program(&program) { + for error in errors { + println!("Type error: {}", error); + } + return; + } + + // Execute + match interp.run(&program) { + Ok(value) => { + if !matches!(value, interpreter::Value::Unit) { + println!("{}", value); + } + } + Err(e) => { + println!("Runtime error: {}", e); + } + } + } + Err(parse_err) => { + // Try wrapping as an expression + let wrapped = format!("let _result_ = {}", input.trim()); + match Parser::parse_source(&wrapped) { + Ok(program) => { + if let Err(errors) = checker.check_program(&program) { + for error in errors { + println!("Type error: {}", error); + } + return; + } + + match interp.run(&program) { + Ok(value) => { + println!("{}", value); + } + Err(e) => { + println!("Runtime error: {}", e); + } + } + } + Err(_) => { + // Use original error + println!("Parse error: {}", parse_err); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn eval(source: &str) -> Result { + let program = Parser::parse_source(source).map_err(|e| e.to_string())?; + + let mut checker = TypeChecker::new(); + checker.check_program(&program).map_err(|errors| { + errors + .iter() + .map(|e| e.to_string()) + .collect::>() + .join("\n") + })?; + + let mut interp = Interpreter::new(); + let value = interp.run(&program).map_err(|e| e.to_string())?; + Ok(format!("{}", value)) + } + + #[test] + fn test_arithmetic() { + assert_eq!(eval("let x = 1 + 2").unwrap(), "3"); + assert_eq!(eval("let x = 10 - 3").unwrap(), "7"); + assert_eq!(eval("let x = 4 * 5").unwrap(), "20"); + assert_eq!(eval("let x = 15 / 3").unwrap(), "5"); + } + + #[test] + fn test_function() { + let source = r#" + fn add(a: Int, b: Int): Int = a + b + let result = add(3, 4) + "#; + assert_eq!(eval(source).unwrap(), "7"); + } + + #[test] + fn test_if_expr() { + let source = r#" + fn max(a: Int, b: Int): Int = if a > b then a else b + let result = max(5, 3) + "#; + assert_eq!(eval(source).unwrap(), "5"); + } + + #[test] + fn test_recursion() { + let source = r#" + fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1) + let result = factorial(5) + "#; + assert_eq!(eval(source).unwrap(), "120"); + } + + #[test] + fn test_lambda() { + let source = r#" + let double = fn(x: Int): Int => x * 2 + let result = double(21) + "#; + assert_eq!(eval(source).unwrap(), "42"); + } + + #[test] + fn test_records() { + let source = r#" + let person = { name: "Alice", age: 30 } + let result = person.age + "#; + assert_eq!(eval(source).unwrap(), "30"); + } + + #[test] + fn test_lists() { + let source = "let nums = [1, 2, 3]"; + assert_eq!(eval(source).unwrap(), "[1, 2, 3]"); + } + + #[test] + fn test_tuples() { + let source = "let pair = (42, \"hello\")"; + assert_eq!(eval(source).unwrap(), "(42, \"hello\")"); + } + + #[test] + fn test_block() { + let source = r#" + let result = { + let x = 10 + let y = 20 + x + y + } + "#; + assert_eq!(eval(source).unwrap(), "30"); + } + + #[test] + fn test_pipe() { + let source = r#" + fn double(x: Int): Int = x * 2 + fn add_one(x: Int): Int = x + 1 + let result = 5 |> double |> add_one + "#; + assert_eq!(eval(source).unwrap(), "11"); + } + + // ============ Standard Library Tests ============ + + // List tests + #[test] + fn test_list_length() { + assert_eq!(eval("let x = List.length([1, 2, 3])").unwrap(), "3"); + assert_eq!(eval("let x = List.length([])").unwrap(), "0"); + } + + #[test] + fn test_list_reverse() { + assert_eq!( + eval("let x = List.reverse([1, 2, 3])").unwrap(), + "[3, 2, 1]" + ); + assert_eq!(eval("let x = List.reverse([])").unwrap(), "[]"); + } + + #[test] + fn test_list_range() { + assert_eq!(eval("let x = List.range(0, 5)").unwrap(), "[0, 1, 2, 3, 4]"); + assert_eq!(eval("let x = List.range(3, 3)").unwrap(), "[]"); + assert_eq!(eval("let x = List.range(-2, 2)").unwrap(), "[-2, -1, 0, 1]"); + } + + #[test] + fn test_list_head() { + assert_eq!(eval("let x = List.head([1, 2, 3])").unwrap(), "Some(1)"); + assert_eq!(eval("let x = List.head([])").unwrap(), "None"); + } + + #[test] + fn test_list_tail() { + assert_eq!( + eval("let x = List.tail([1, 2, 3])").unwrap(), + "Some([2, 3])" + ); + assert_eq!(eval("let x = List.tail([1])").unwrap(), "Some([])"); + assert_eq!(eval("let x = List.tail([])").unwrap(), "None"); + } + + #[test] + fn test_list_concat() { + assert_eq!( + eval("let x = List.concat([1, 2], [3, 4])").unwrap(), + "[1, 2, 3, 4]" + ); + assert_eq!(eval("let x = List.concat([], [1])").unwrap(), "[1]"); + assert_eq!(eval("let x = List.concat([1], [])").unwrap(), "[1]"); + } + + #[test] + fn test_list_get() { + assert_eq!( + eval("let x = List.get([10, 20, 30], 0)").unwrap(), + "Some(10)" + ); + assert_eq!( + eval("let x = List.get([10, 20, 30], 2)").unwrap(), + "Some(30)" + ); + assert_eq!(eval("let x = List.get([10, 20, 30], 5)").unwrap(), "None"); + assert_eq!(eval("let x = List.get([10, 20, 30], -1)").unwrap(), "None"); + } + + #[test] + fn test_list_map() { + let source = r#" + fn double(x: Int): Int = x * 2 + let result = List.map([1, 2, 3], double) + "#; + assert_eq!(eval(source).unwrap(), "[2, 4, 6]"); + } + + #[test] + fn test_list_map_lambda() { + let source = "let x = List.map([1, 2, 3], fn(x: Int): Int => x * x)"; + assert_eq!(eval(source).unwrap(), "[1, 4, 9]"); + } + + #[test] + fn test_list_filter() { + let source = "let x = List.filter([1, 2, 3, 4, 5], fn(x: Int): Bool => x > 2)"; + assert_eq!(eval(source).unwrap(), "[3, 4, 5]"); + } + + #[test] + fn test_list_filter_all() { + let source = "let x = List.filter([1, 2, 3], fn(x: Int): Bool => x > 10)"; + assert_eq!(eval(source).unwrap(), "[]"); + } + + #[test] + fn test_list_fold() { + let source = "let x = List.fold([1, 2, 3, 4], 0, fn(acc: Int, x: Int): Int => acc + x)"; + assert_eq!(eval(source).unwrap(), "10"); + } + + #[test] + fn test_list_fold_product() { + let source = "let x = List.fold([1, 2, 3, 4], 1, fn(acc: Int, x: Int): Int => acc * x)"; + assert_eq!(eval(source).unwrap(), "24"); + } + + // String tests + #[test] + fn test_string_length() { + assert_eq!(eval(r#"let x = String.length("hello")"#).unwrap(), "5"); + assert_eq!(eval(r#"let x = String.length("")"#).unwrap(), "0"); + } + + #[test] + fn test_string_split() { + assert_eq!( + eval(r#"let x = String.split("a,b,c", ",")"#).unwrap(), + r#"["a", "b", "c"]"# + ); + assert_eq!( + eval(r#"let x = String.split("hello", ",")"#).unwrap(), + r#"["hello"]"# + ); + } + + #[test] + fn test_string_join() { + assert_eq!( + eval(r#"let x = String.join(["a", "b", "c"], "-")"#).unwrap(), + r#""a-b-c""# + ); + assert_eq!( + eval(r#"let x = String.join(["hello"], ",")"#).unwrap(), + r#""hello""# + ); + assert_eq!(eval(r#"let x = String.join([], ",")"#).unwrap(), r#""""#); + } + + #[test] + fn test_string_trim() { + assert_eq!( + eval(r#"let x = String.trim(" hello ")"#).unwrap(), + r#""hello""# + ); + assert_eq!( + eval(r#"let x = String.trim("hello")"#).unwrap(), + r#""hello""# + ); + assert_eq!(eval(r#"let x = String.trim(" ")"#).unwrap(), r#""""#); + } + + #[test] + fn test_string_contains() { + assert_eq!( + eval(r#"let x = String.contains("hello world", "world")"#).unwrap(), + "true" + ); + assert_eq!( + eval(r#"let x = String.contains("hello", "xyz")"#).unwrap(), + "false" + ); + assert_eq!( + eval(r#"let x = String.contains("hello", "")"#).unwrap(), + "true" + ); + } + + #[test] + fn test_string_replace() { + assert_eq!( + eval(r#"let x = String.replace("hello", "l", "L")"#).unwrap(), + r#""heLLo""# + ); + assert_eq!( + eval(r#"let x = String.replace("aaa", "a", "b")"#).unwrap(), + r#""bbb""# + ); + } + + #[test] + fn test_string_chars() { + assert_eq!(eval(r#"let x = String.chars("hi")"#).unwrap(), "['h', 'i']"); + assert_eq!(eval(r#"let x = String.chars("")"#).unwrap(), "[]"); + } + + #[test] + fn test_string_lines() { + // Note: Using actual newline in the string + let source = r#"let x = String.lines("a +b +c")"#; + assert_eq!(eval(source).unwrap(), r#"["a", "b", "c"]"#); + } + + // Option tests + #[test] + fn test_option_constructors() { + assert_eq!(eval("let x = Some(42)").unwrap(), "Some(42)"); + assert_eq!(eval("let x = None").unwrap(), "None"); + } + + #[test] + fn test_option_is_some() { + assert_eq!(eval("let x = Option.isSome(Some(42))").unwrap(), "true"); + assert_eq!(eval("let x = Option.isSome(None)").unwrap(), "false"); + } + + #[test] + fn test_option_is_none() { + assert_eq!(eval("let x = Option.isNone(None)").unwrap(), "true"); + assert_eq!(eval("let x = Option.isNone(Some(42))").unwrap(), "false"); + } + + #[test] + fn test_option_get_or_else() { + assert_eq!(eval("let x = Option.getOrElse(Some(42), 0)").unwrap(), "42"); + assert_eq!(eval("let x = Option.getOrElse(None, 0)").unwrap(), "0"); + } + + #[test] + fn test_option_map() { + let source = "let x = Option.map(Some(5), fn(x: Int): Int => x * 2)"; + assert_eq!(eval(source).unwrap(), "Some(10)"); + } + + #[test] + fn test_option_map_none() { + let source = "let x = Option.map(None, fn(x: Int): Int => x * 2)"; + assert_eq!(eval(source).unwrap(), "None"); + } + + #[test] + fn test_option_flat_map() { + let source = "let x = Option.flatMap(Some(5), fn(x: Int): Option => Some(x * 2))"; + assert_eq!(eval(source).unwrap(), "Some(10)"); + } + + #[test] + fn test_option_flat_map_to_none() { + let source = "let x = Option.flatMap(Some(5), fn(x: Int): Option => None)"; + assert_eq!(eval(source).unwrap(), "None"); + } + + // Result tests + #[test] + fn test_result_constructors() { + assert_eq!(eval("let x = Ok(42)").unwrap(), "Ok(42)"); + assert_eq!(eval(r#"let x = Err("error")"#).unwrap(), r#"Err("error")"#); + } + + #[test] + fn test_result_is_ok() { + assert_eq!(eval("let x = Result.isOk(Ok(42))").unwrap(), "true"); + assert_eq!(eval(r#"let x = Result.isOk(Err("e"))"#).unwrap(), "false"); + } + + #[test] + fn test_result_is_err() { + assert_eq!(eval(r#"let x = Result.isErr(Err("e"))"#).unwrap(), "true"); + assert_eq!(eval("let x = Result.isErr(Ok(42))").unwrap(), "false"); + } + + #[test] + fn test_result_get_or_else() { + assert_eq!(eval("let x = Result.getOrElse(Ok(42), 0)").unwrap(), "42"); + assert_eq!( + eval(r#"let x = Result.getOrElse(Err("e"), 0)"#).unwrap(), + "0" + ); + } + + #[test] + fn test_result_map() { + let source = "let x = Result.map(Ok(5), fn(x: Int): Int => x * 2)"; + assert_eq!(eval(source).unwrap(), "Ok(10)"); + } + + #[test] + fn test_result_map_err() { + let source = r#"let x = Result.map(Err("e"), fn(x: Int): Int => x * 2)"#; + assert_eq!(eval(source).unwrap(), r#"Err("e")"#); + } + + // Utility function tests + #[test] + fn test_to_string() { + assert_eq!(eval("let x = toString(42)").unwrap(), r#""42""#); + assert_eq!(eval("let x = toString(true)").unwrap(), r#""true""#); + assert_eq!(eval("let x = toString([1, 2])").unwrap(), r#""[1, 2]""#); + } + + #[test] + fn test_type_of() { + assert_eq!(eval("let x = typeOf(42)").unwrap(), r#""Int""#); + assert_eq!(eval("let x = typeOf(true)").unwrap(), r#""Bool""#); + assert_eq!(eval("let x = typeOf([1, 2])").unwrap(), r#""List""#); + assert_eq!(eval(r#"let x = typeOf("hello")"#).unwrap(), r#""String""#); + } + + // Pipe with stdlib tests + #[test] + fn test_pipe_with_list() { + assert_eq!( + eval("let x = [1, 2, 3] |> List.reverse").unwrap(), + "[3, 2, 1]" + ); + assert_eq!(eval("let x = [1, 2, 3] |> List.length").unwrap(), "3"); + } + + #[test] + fn test_pipe_with_string() { + assert_eq!( + eval(r#"let x = " hello " |> String.trim"#).unwrap(), + r#""hello""# + ); + } + + // Combined stdlib usage tests + #[test] + fn test_list_filter_even() { + let source = r#" + fn isEven(x: Int): Bool = x % 2 == 0 + let result = List.filter(List.range(1, 6), isEven) + "#; + assert_eq!(eval(source).unwrap(), "[2, 4]"); + } + + #[test] + fn test_option_chain() { + let source = r#" + fn times10(x: Int): Int = x * 10 + let head = List.head([1, 2, 3]) + let mapped = Option.map(head, times10) + let result = Option.getOrElse(mapped, 0) + "#; + assert_eq!(eval(source).unwrap(), "10"); + } + + #[test] + fn test_option_chain_empty() { + let source = r#" + fn times10(x: Int): Int = x * 10 + let head = List.head([]) + let mapped = Option.map(head, times10) + let result = Option.getOrElse(mapped, 0) + "#; + assert_eq!(eval(source).unwrap(), "0"); + } +} diff --git a/src/modules.rs b/src/modules.rs new file mode 100644 index 0000000..9cc3d74 --- /dev/null +++ b/src/modules.rs @@ -0,0 +1,634 @@ +//! Module system for the Lux language +//! +//! Handles loading, parsing, and resolving module imports. + +use crate::ast::{Declaration, ImportDecl, Program, Visibility}; +use crate::parser::Parser; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Error during module loading +#[derive(Debug, Clone)] +pub struct ModuleError { + pub message: String, + pub module_path: String, +} + +impl std::fmt::Display for ModuleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Module error in '{}': {}", + self.module_path, self.message + ) + } +} + +impl std::error::Error for ModuleError {} + +/// A loaded and parsed module +#[derive(Debug, Clone)] +pub struct Module { + /// The module's canonical path (e.g., "std/list") + pub path: String, + /// The parsed program + pub program: Program, + /// Names exported by this module (public declarations) + pub exports: HashSet, +} + +impl Module { + /// Get all public declarations from this module + pub fn public_declarations(&self) -> Vec<&Declaration> { + self.program + .declarations + .iter() + .filter(|d| { + match d { + Declaration::Function(f) => f.visibility == Visibility::Public, + Declaration::Let(l) => l.visibility == Visibility::Public, + Declaration::Type(t) => t.visibility == Visibility::Public, + // Effects and handlers are always public for now + Declaration::Effect(_) | Declaration::Handler(_) => true, + } + }) + .collect() + } +} + +/// Module loader and resolver +pub struct ModuleLoader { + /// Base directories to search for modules + search_paths: Vec, + /// Cache of loaded modules (path -> module) + cache: HashMap, + /// Modules currently being loaded (for circular dependency detection) + loading: HashSet, +} + +impl ModuleLoader { + pub fn new() -> Self { + Self { + search_paths: vec![PathBuf::from(".")], + cache: HashMap::new(), + loading: HashSet::new(), + } + } + + /// Create a loader with custom search paths + pub fn with_paths(paths: Vec) -> Self { + Self { + search_paths: paths, + cache: HashMap::new(), + loading: HashSet::new(), + } + } + + /// Add a search path + pub fn add_search_path(&mut self, path: PathBuf) { + self.search_paths.push(path); + } + + /// Resolve a module path to a file path + fn resolve_path(&self, module_path: &str) -> Option { + // Convert module path (e.g., "std/list") to file path (e.g., "std/list.lux") + let relative_path = format!("{}.lux", module_path); + + for search_path in &self.search_paths { + let full_path = search_path.join(&relative_path); + if full_path.exists() { + return Some(full_path); + } + } + + None + } + + /// Load a module by its import path + pub fn load_module(&mut self, module_path: &str) -> Result<&Module, ModuleError> { + // Check if already cached + if self.cache.contains_key(module_path) { + return Ok(self.cache.get(module_path).unwrap()); + } + + // Check for circular dependency + if self.loading.contains(module_path) { + return Err(ModuleError { + message: "Circular dependency detected".to_string(), + module_path: module_path.to_string(), + }); + } + + // Mark as loading + self.loading.insert(module_path.to_string()); + + // Resolve to file path + let file_path = self.resolve_path(module_path).ok_or_else(|| ModuleError { + message: format!("Module not found. Searched in: {:?}", self.search_paths), + module_path: module_path.to_string(), + })?; + + // Load the module + let module = self.load_file(&file_path, module_path)?; + + // Remove from loading set + self.loading.remove(module_path); + + // Cache the module + self.cache.insert(module_path.to_string(), module); + + Ok(self.cache.get(module_path).unwrap()) + } + + /// Load a module from a file path + fn load_file(&mut self, file_path: &Path, module_path: &str) -> Result { + // Read the file + let source = fs::read_to_string(file_path).map_err(|e| ModuleError { + message: format!("Failed to read file: {}", e), + module_path: module_path.to_string(), + })?; + + // Parse the source + let program = Parser::parse_source(&source).map_err(|e| ModuleError { + message: format!("Parse error: {}", e), + module_path: module_path.to_string(), + })?; + + // Load any imports this module has + for import in &program.imports { + let import_path = import.path.to_string(); + self.load_module(&import_path)?; + } + + // Collect exports + let exports = self.collect_exports(&program); + + Ok(Module { + path: module_path.to_string(), + program, + exports, + }) + } + + /// Load a program from source (for REPL or direct execution) + pub fn load_source( + &mut self, + source: &str, + base_path: Option<&Path>, + ) -> Result { + // Add base path to search paths if provided + if let Some(base) = base_path { + if let Some(parent) = base.parent() { + if !self.search_paths.contains(&parent.to_path_buf()) { + self.search_paths.push(parent.to_path_buf()); + } + } + } + + // Parse the source + let program = Parser::parse_source(source).map_err(|e| ModuleError { + message: format!("Parse error: {}", e), + module_path: "
".to_string(), + })?; + + // Load any imports + for import in &program.imports { + let import_path = import.path.to_string(); + self.load_module(&import_path)?; + } + + Ok(program) + } + + /// Collect exported names from a program + fn collect_exports(&self, program: &Program) -> HashSet { + let mut exports = HashSet::new(); + + for decl in &program.declarations { + match decl { + Declaration::Function(f) if f.visibility == Visibility::Public => { + exports.insert(f.name.name.clone()); + } + Declaration::Let(l) if l.visibility == Visibility::Public => { + exports.insert(l.name.name.clone()); + } + Declaration::Type(t) if t.visibility == Visibility::Public => { + exports.insert(t.name.name.clone()); + } + Declaration::Effect(e) => { + // Effects are always exported + exports.insert(e.name.name.clone()); + } + Declaration::Handler(h) => { + // Handlers are always exported + exports.insert(h.name.name.clone()); + } + _ => {} + } + } + + exports + } + + /// Get a cached module + pub fn get_module(&self, module_path: &str) -> Option<&Module> { + self.cache.get(module_path) + } + + /// Get all loaded modules + pub fn loaded_modules(&self) -> impl Iterator { + self.cache.iter() + } + + /// Clear the module cache + pub fn clear_cache(&mut self) { + self.cache.clear(); + } + + /// Resolve imports for a program and return the names to be imported + pub fn resolve_imports( + &self, + imports: &[ImportDecl], + ) -> Result, ModuleError> { + let mut resolved = HashMap::new(); + + for import in imports { + let module_path = import.path.to_string(); + let module = self.get_module(&module_path).ok_or_else(|| ModuleError { + message: "Module not loaded".to_string(), + module_path: module_path.clone(), + })?; + + let import_name = if let Some(ref alias) = import.alias { + // import foo/bar as Baz -> use "Baz" as the name + alias.name.clone() + } else { + // import foo/bar -> use "bar" as the name (last segment) + import + .path + .segments + .last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| module_path.clone()) + }; + + if import.wildcard { + // import foo.* -> import all exports directly + for export in &module.exports { + resolved.insert( + export.clone(), + ResolvedImport { + module_path: module_path.clone(), + name: export.clone(), + kind: ImportKind::Direct, + }, + ); + } + } else if let Some(ref items) = import.items { + // import foo.{a, b, c} -> import specific items + for item in items { + if !module.exports.contains(&item.name) { + return Err(ModuleError { + message: format!("'{}' is not exported from module", item.name), + module_path: module_path.clone(), + }); + } + resolved.insert( + item.name.clone(), + ResolvedImport { + module_path: module_path.clone(), + name: item.name.clone(), + kind: ImportKind::Direct, + }, + ); + } + } else { + // import foo/bar -> import as module object + resolved.insert( + import_name, + ResolvedImport { + module_path: module_path.clone(), + name: module_path.clone(), + kind: ImportKind::Module, + }, + ); + } + } + + Ok(resolved) + } +} + +impl Default for ModuleLoader { + fn default() -> Self { + Self::new() + } +} + +/// A resolved import +#[derive(Debug, Clone)] +pub struct ResolvedImport { + /// The module path this import comes from + pub module_path: String, + /// The name being imported + pub name: String, + /// What kind of import this is + pub kind: ImportKind, +} + +/// Kind of import +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ImportKind { + /// Import as a module object (import foo/bar) + Module, + /// Direct import of a name (import foo.{bar} or import foo.*) + Direct, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::TempDir; + + fn create_test_module(dir: &Path, name: &str, content: &str) -> PathBuf { + let path = dir.join(format!("{}.lux", name)); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + let mut file = fs::File::create(&path).unwrap(); + file.write_all(content.as_bytes()).unwrap(); + path + } + + #[test] + fn test_load_simple_module() { + let dir = TempDir::new().unwrap(); + create_test_module( + dir.path(), + "math", + r#" + pub fn add(a: Int, b: Int): Int = a + b + pub fn sub(a: Int, b: Int): Int = a - b + fn private_fn(): Int = 42 + "#, + ); + + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + let module = loader.load_module("math").unwrap(); + + assert_eq!(module.path, "math"); + assert!(module.exports.contains("add")); + assert!(module.exports.contains("sub")); + assert!(!module.exports.contains("private_fn")); + } + + #[test] + fn test_load_nested_module() { + let dir = TempDir::new().unwrap(); + create_test_module( + dir.path(), + "std/list", + r#" + pub fn length(list: List): Int = 0 + "#, + ); + + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + let module = loader.load_module("std/list").unwrap(); + + assert_eq!(module.path, "std/list"); + assert!(module.exports.contains("length")); + } + + #[test] + fn test_module_not_found() { + let dir = TempDir::new().unwrap(); + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + + let result = loader.load_module("nonexistent"); + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("not found")); + } + + #[test] + fn test_circular_dependency_detection() { + let dir = TempDir::new().unwrap(); + create_test_module( + dir.path(), + "a", + r#" + import b + pub fn foo(): Int = 1 + "#, + ); + create_test_module( + dir.path(), + "b", + r#" + import a + pub fn bar(): Int = 2 + "#, + ); + + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + let result = loader.load_module("a"); + + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("Circular")); + } + + #[test] + fn test_module_caching() { + let dir = TempDir::new().unwrap(); + create_test_module( + dir.path(), + "cached", + r#" + pub fn foo(): Int = 42 + "#, + ); + + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + + // Load twice + loader.load_module("cached").unwrap(); + loader.load_module("cached").unwrap(); + + // Should only be in cache once + assert_eq!(loader.cache.len(), 1); + } + + #[test] + fn test_end_to_end_module_import() { + use crate::interpreter::Interpreter; + use crate::typechecker::TypeChecker; + + let dir = TempDir::new().unwrap(); + + // Create a utility module with public functions + create_test_module( + dir.path(), + "utils", + r#" + pub fn double(x: Int): Int = x * 2 + pub fn square(x: Int): Int = x * x + fn private_helper(): Int = 0 + "#, + ); + + // Create a main program that imports and uses the module + let main_source = r#" + import utils + + let result = utils.double(21) + "#; + + // Set up module loader + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + + // Load and parse the main program + let main_path = dir.path().join("main.lux"); + let program = loader.load_source(main_source, Some(&main_path)).unwrap(); + + // Type check with module support + let mut checker = TypeChecker::new(); + checker + .check_program_with_modules(&program, &loader) + .unwrap(); + + // Run with module support + let mut interp = Interpreter::new(); + let result = interp.run_with_modules(&program, &loader).unwrap(); + + // Should evaluate to 42 + assert_eq!(format!("{}", result), "42"); + } + + #[test] + fn test_selective_import() { + use crate::interpreter::Interpreter; + use crate::typechecker::TypeChecker; + + let dir = TempDir::new().unwrap(); + + // Create a module with multiple exports + create_test_module( + dir.path(), + "math", + r#" + pub fn add(a: Int, b: Int): Int = a + b + pub fn mul(a: Int, b: Int): Int = a * b + "#, + ); + + // Import only the add function + let main_source = r#" + import math.{add} + + let result = add(10, 5) + "#; + + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + let main_path = dir.path().join("main.lux"); + let program = loader.load_source(main_source, Some(&main_path)).unwrap(); + + let mut checker = TypeChecker::new(); + checker + .check_program_with_modules(&program, &loader) + .unwrap(); + + let mut interp = Interpreter::new(); + let result = interp.run_with_modules(&program, &loader).unwrap(); + + assert_eq!(format!("{}", result), "15"); + } + + #[test] + fn test_module_with_alias() { + use crate::interpreter::Interpreter; + use crate::typechecker::TypeChecker; + + let dir = TempDir::new().unwrap(); + + // Create a nested module + create_test_module( + dir.path(), + "lib/helpers", + r#" + pub fn greet(): String = "hello" + "#, + ); + + // Import with alias + let main_source = r#" + import lib/helpers as h + + let result = h.greet() + "#; + + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + let main_path = dir.path().join("main.lux"); + let program = loader.load_source(main_source, Some(&main_path)).unwrap(); + + let mut checker = TypeChecker::new(); + checker + .check_program_with_modules(&program, &loader) + .unwrap(); + + let mut interp = Interpreter::new(); + let result = interp.run_with_modules(&program, &loader).unwrap(); + + assert_eq!(format!("{}", result), "\"hello\""); + } + + #[test] + fn test_transitive_imports() { + use crate::interpreter::Interpreter; + use crate::typechecker::TypeChecker; + + let dir = TempDir::new().unwrap(); + + // Create base module + create_test_module( + dir.path(), + "base", + r#" + pub fn value(): Int = 100 + "#, + ); + + // Create mid module that imports base + create_test_module( + dir.path(), + "mid", + r#" + import base + + pub fn doubled(): Int = base.value() * 2 + "#, + ); + + // Create main that imports mid + let main_source = r#" + import mid + + let result = mid.doubled() + "#; + + let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); + let main_path = dir.path().join("main.lux"); + let program = loader.load_source(main_source, Some(&main_path)).unwrap(); + + let mut checker = TypeChecker::new(); + checker + .check_program_with_modules(&program, &loader) + .unwrap(); + + let mut interp = Interpreter::new(); + let result = interp.run_with_modules(&program, &loader).unwrap(); + + assert_eq!(format!("{}", result), "200"); + } +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..8700354 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,1935 @@ +//! Parser for the Lux language + +use crate::ast::*; +use crate::lexer::{LexError, Lexer, Token, TokenKind}; +use std::fmt; + +/// Parser error +#[derive(Debug, Clone)] +pub struct ParseError { + pub message: String, + pub span: Span, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Parse error at {}-{}: {}", + self.span.start, self.span.end, self.message + ) + } +} + +impl From for ParseError { + fn from(err: LexError) -> Self { + ParseError { + message: err.message, + span: err.span, + } + } +} + +/// The parser +pub struct Parser { + tokens: Vec, + pos: usize, + eof_token: Token, +} + +impl Parser { + pub fn new(tokens: Vec) -> Self { + Self { + tokens, + pos: 0, + eof_token: Token::new(TokenKind::Eof, Span::default()), + } + } + + pub fn parse_source(source: &str) -> Result { + let tokens = Lexer::new(source).tokenize()?; + let mut parser = Parser::new(tokens); + parser.parse_program() + } + + /// Parse a complete program + pub fn parse_program(&mut self) -> Result { + let mut imports = Vec::new(); + let mut declarations = Vec::new(); + self.skip_newlines(); + + // Parse imports first (they must come before declarations) + while self.check(TokenKind::Import) { + imports.push(self.parse_import()?); + self.skip_newlines(); + } + + while !self.is_at_end() { + declarations.push(self.parse_declaration()?); + self.skip_newlines(); + } + + Ok(Program { + imports, + declarations, + }) + } + + /// Parse an import declaration + fn parse_import(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Import)?; + + // Parse module path: foo/bar/baz + let mut segments = vec![self.parse_ident()?]; + while self.check(TokenKind::Slash) { + self.advance(); + segments.push(self.parse_ident()?); + } + let path_span = start.merge(self.previous_span()); + let path = ModulePath { + segments, + span: path_span, + }; + + // Check for wildcard import: import foo.* + let wildcard = if self.check(TokenKind::Dot) && self.peek_next_kind() == TokenKind::Star { + self.advance(); // . + self.advance(); // * + true + } else { + false + }; + + // Check for selective import: import foo.{a, b, c} + let items = if !wildcard + && self.check(TokenKind::Dot) + && self.peek_next_kind() == TokenKind::LBrace + { + self.advance(); // . + self.advance(); // { + let mut items = Vec::new(); + while !self.check(TokenKind::RBrace) { + items.push(self.parse_ident()?); + if !self.check(TokenKind::RBrace) { + self.expect(TokenKind::Comma)?; + } + } + self.expect(TokenKind::RBrace)?; + Some(items) + } else { + None + }; + + // Check for alias: import foo as bar + let alias = if self.check(TokenKind::As) { + self.advance(); + Some(self.parse_ident()?) + } else { + None + }; + + let span = start.merge(self.previous_span()); + Ok(ImportDecl { + path, + alias, + items, + wildcard, + span, + }) + } + + /// Parse a top-level declaration + fn parse_declaration(&mut self) -> Result { + self.skip_newlines(); + + // Check for visibility modifier + let visibility = if self.check(TokenKind::Pub) { + self.advance(); + Visibility::Public + } else { + Visibility::Private + }; + + match self.peek_kind() { + TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility)?)), + TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl()?)), + TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)), + TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility)?)), + TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility)?)), + _ => Err(self.error("Expected declaration (fn, effect, handler, type, or let)")), + } + } + + /// Parse a function declaration + fn parse_function_decl(&mut self, visibility: Visibility) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Fn)?; + + let name = self.parse_ident()?; + + // Optional type parameters + let type_params = if self.check(TokenKind::Lt) { + self.parse_type_params()? + } else { + Vec::new() + }; + + self.expect(TokenKind::LParen)?; + let params = self.parse_params()?; + self.expect(TokenKind::RParen)?; + + // Return type + self.expect(TokenKind::Colon)?; + let return_type = self.parse_type()?; + + // Optional effects + let effects = if self.check(TokenKind::With) { + self.advance(); + self.parse_effect_list()? + } else { + Vec::new() + }; + + self.expect(TokenKind::Eq)?; + self.skip_newlines(); + let body = self.parse_expr()?; + + let span = start.merge(body.span()); + Ok(FunctionDecl { + visibility, + name, + type_params, + params, + return_type, + effects, + body, + span, + }) + } + + /// Parse effect declaration + fn parse_effect_decl(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Effect)?; + + let name = self.parse_ident()?; + + let type_params = if self.check(TokenKind::Lt) { + self.parse_type_params()? + } else { + Vec::new() + }; + + self.expect(TokenKind::LBrace)?; + self.skip_newlines(); + + let mut operations = Vec::new(); + while !self.check(TokenKind::RBrace) { + operations.push(self.parse_effect_op()?); + self.skip_newlines(); + // Optional comma or newline separator + if self.check(TokenKind::Comma) { + self.advance(); + } + self.skip_newlines(); + } + + let end = self.current_span(); + self.expect(TokenKind::RBrace)?; + + Ok(EffectDecl { + name, + type_params, + operations, + span: start.merge(end), + }) + } + + /// Parse an effect operation + fn parse_effect_op(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Fn)?; + + let name = self.parse_ident()?; + + self.expect(TokenKind::LParen)?; + let params = self.parse_params()?; + self.expect(TokenKind::RParen)?; + + self.expect(TokenKind::Colon)?; + let return_type = self.parse_type()?; + + let span = start.merge(self.previous_span()); + Ok(EffectOp { + name, + params, + return_type, + span, + }) + } + + /// Parse handler declaration + fn parse_handler_decl(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Handler)?; + + let name = self.parse_ident()?; + + // Optional parameters + let params = if self.check(TokenKind::LParen) { + self.advance(); + let p = self.parse_params()?; + self.expect(TokenKind::RParen)?; + p + } else { + Vec::new() + }; + + self.expect(TokenKind::Colon)?; + let effect = self.parse_ident()?; + + self.expect(TokenKind::LBrace)?; + self.skip_newlines(); + + let mut implementations = Vec::new(); + while !self.check(TokenKind::RBrace) { + implementations.push(self.parse_handler_impl()?); + self.skip_newlines(); + } + + let end = self.current_span(); + self.expect(TokenKind::RBrace)?; + + Ok(HandlerDecl { + name, + params, + effect, + implementations, + span: start.merge(end), + }) + } + + /// Parse handler implementation + fn parse_handler_impl(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Fn)?; + + let op_name = self.parse_ident()?; + + self.expect(TokenKind::LParen)?; + let mut params = Vec::new(); + while !self.check(TokenKind::RParen) { + params.push(self.parse_ident()?); + if !self.check(TokenKind::RParen) { + self.expect(TokenKind::Comma)?; + } + } + self.expect(TokenKind::RParen)?; + + // Optional resume parameter + let resume = if self.check(TokenKind::With) { + self.advance(); + Some(self.parse_ident()?) + } else { + None + }; + + self.expect(TokenKind::Eq)?; + self.skip_newlines(); + let body = self.parse_expr()?; + + let span = start.merge(body.span()); + Ok(HandlerImpl { + op_name, + params, + resume, + body, + span, + }) + } + + /// Parse type declaration + fn parse_type_decl(&mut self, visibility: Visibility) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Type)?; + + let name = self.parse_ident()?; + + let type_params = if self.check(TokenKind::Lt) { + self.parse_type_params()? + } else { + Vec::new() + }; + + // Check for version annotation: type User @v2 { ... } + let version = if self.check(TokenKind::At) { + Some(self.parse_version()?) + } else { + None + }; + + // Check what kind of type definition + let definition = if self.check(TokenKind::LBrace) { + // Record type + self.advance(); + self.skip_newlines(); + let fields = self.parse_record_fields()?; + self.skip_newlines(); + + // Check for migrations inside the record: from @v1 = { ... } + let migrations = self.parse_migrations()?; + + self.expect(TokenKind::RBrace)?; + (TypeDef::Record(fields), migrations) + } else if self.check(TokenKind::Eq) { + self.advance(); + self.skip_newlines(); + + if self.check(TokenKind::Pipe) || self.peek_is_variant() { + // Enum type + (TypeDef::Enum(self.parse_variants()?), Vec::new()) + } else { + // Type alias + (TypeDef::Alias(self.parse_type()?), Vec::new()) + } + } else { + return Err(self.error("Expected '=' or '{' in type declaration")); + }; + + let span = start.merge(self.previous_span()); + Ok(TypeDecl { + visibility, + name, + type_params, + version, + definition: definition.0, + migrations: definition.1, + span, + }) + } + + /// Parse let declaration + fn parse_let_decl(&mut self, visibility: Visibility) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Let)?; + + let name = self.parse_ident()?; + + let typ = if self.check(TokenKind::Colon) { + self.advance(); + Some(self.parse_type()?) + } else { + None + }; + + self.expect(TokenKind::Eq)?; + self.skip_newlines(); + let value = self.parse_expr()?; + + let span = start.merge(value.span()); + Ok(LetDecl { + visibility, + name, + typ, + value, + span, + }) + } + + /// Parse type parameters + fn parse_type_params(&mut self) -> Result, ParseError> { + self.expect(TokenKind::Lt)?; + let mut params = Vec::new(); + + while !self.check(TokenKind::Gt) { + params.push(self.parse_ident()?); + if !self.check(TokenKind::Gt) { + self.expect(TokenKind::Comma)?; + } + } + + self.expect(TokenKind::Gt)?; + Ok(params) + } + + /// Parse function parameters + fn parse_params(&mut self) -> Result, ParseError> { + let mut params = Vec::new(); + + while !self.check(TokenKind::RParen) { + let start = self.current_span(); + let name = self.parse_ident()?; + self.expect(TokenKind::Colon)?; + let typ = self.parse_type()?; + let span = start.merge(self.previous_span()); + + params.push(Parameter { name, typ, span }); + + if !self.check(TokenKind::RParen) { + self.expect(TokenKind::Comma)?; + } + } + + Ok(params) + } + + /// Parse effect list { Effect1, Effect2 } + fn parse_effect_list(&mut self) -> Result, ParseError> { + self.expect(TokenKind::LBrace)?; + let mut effects = Vec::new(); + + while !self.check(TokenKind::RBrace) { + effects.push(self.parse_ident()?); + if !self.check(TokenKind::RBrace) { + self.expect(TokenKind::Comma)?; + } + } + + self.expect(TokenKind::RBrace)?; + Ok(effects) + } + + /// Parse a type expression + fn parse_type(&mut self) -> Result { + // Function type: fn(A, B): C with {E} + if self.check(TokenKind::Fn) { + return self.parse_function_type(); + } + + // Tuple or parenthesized type + if self.check(TokenKind::LParen) { + return self.parse_tuple_or_paren_type(); + } + + // Record type + if self.check(TokenKind::LBrace) { + return self.parse_record_type(); + } + + // Named type (possibly with type arguments) + let name = self.parse_ident()?; + + // Check for type arguments + let base_type = if self.check(TokenKind::Lt) { + self.advance(); + let mut args = Vec::new(); + + while !self.check(TokenKind::Gt) { + args.push(self.parse_type()?); + if !self.check(TokenKind::Gt) { + self.expect(TokenKind::Comma)?; + } + } + + self.expect(TokenKind::Gt)?; + TypeExpr::App(Box::new(TypeExpr::Named(name)), args) + } else { + TypeExpr::Named(name) + }; + + // Check for version constraint: Type @v1, Type @v2+, Type @latest + if self.check(TokenKind::At) { + let constraint = self.parse_version_constraint()?; + Ok(TypeExpr::Versioned { + base: Box::new(base_type), + constraint, + }) + } else { + Ok(base_type) + } + } + + fn parse_function_type(&mut self) -> Result { + self.expect(TokenKind::Fn)?; + self.expect(TokenKind::LParen)?; + + let mut params = Vec::new(); + while !self.check(TokenKind::RParen) { + params.push(self.parse_type()?); + if !self.check(TokenKind::RParen) { + self.expect(TokenKind::Comma)?; + } + } + + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::Colon)?; + let return_type = Box::new(self.parse_type()?); + + let effects = if self.check(TokenKind::With) { + self.advance(); + self.parse_effect_list()? + } else { + Vec::new() + }; + + Ok(TypeExpr::Function { + params, + return_type, + effects, + }) + } + + fn parse_tuple_or_paren_type(&mut self) -> Result { + self.expect(TokenKind::LParen)?; + + if self.check(TokenKind::RParen) { + self.advance(); + return Ok(TypeExpr::Unit); + } + + let first = self.parse_type()?; + + if self.check(TokenKind::Comma) { + // Tuple type + let mut elements = vec![first]; + while self.check(TokenKind::Comma) { + self.advance(); + if self.check(TokenKind::RParen) { + break; + } + elements.push(self.parse_type()?); + } + self.expect(TokenKind::RParen)?; + Ok(TypeExpr::Tuple(elements)) + } else { + // Parenthesized type + self.expect(TokenKind::RParen)?; + Ok(first) + } + } + + fn parse_record_type(&mut self) -> Result { + self.expect(TokenKind::LBrace)?; + self.skip_newlines(); + let fields = self.parse_record_fields()?; + self.expect(TokenKind::RBrace)?; + Ok(TypeExpr::Record(fields)) + } + + fn parse_record_fields(&mut self) -> Result, ParseError> { + let mut fields = Vec::new(); + + while !self.check(TokenKind::RBrace) && !self.check(TokenKind::From) { + let start = self.current_span(); + let name = self.parse_ident()?; + self.expect(TokenKind::Colon)?; + let typ = self.parse_type()?; + let span = start.merge(self.previous_span()); + + fields.push(RecordField { name, typ, span }); + + self.skip_newlines(); + if self.check(TokenKind::Comma) { + self.advance(); + } + self.skip_newlines(); + } + + Ok(fields) + } + + /// Parse a version annotation: @v1, @v2, etc. + fn parse_version(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::At)?; + + // Expect an identifier like "v1", "v2" + let ident = self.parse_ident()?; + + // Parse the version number from the identifier (e.g., "v1" -> 1) + if !ident.name.starts_with('v') { + return Err(ParseError { + message: format!("Expected version like @v1, @v2, got @{}", ident.name), + span: ident.span, + }); + } + + let version_str = &ident.name[1..]; + let number = version_str.parse::().map_err(|_| ParseError { + message: format!("Invalid version number: {}", version_str), + span: ident.span, + })?; + + let span = start.merge(ident.span); + Ok(Version::new(number, span)) + } + + /// Parse a version constraint: @v1, @v1+, @latest + fn parse_version_constraint(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::At)?; + + // Check for @latest + if self.check(TokenKind::Latest) { + self.advance(); + let span = start.merge(self.previous_span()); + return Ok(VersionConstraint::Latest(span)); + } + + // Parse version identifier (v1, v2, etc.) + let ident = self.parse_ident()?; + + if !ident.name.starts_with('v') { + return Err(ParseError { + message: format!( + "Expected version like @v1, @v2, or @latest, got @{}", + ident.name + ), + span: ident.span, + }); + } + + let version_str = &ident.name[1..]; + let number = version_str.parse::().map_err(|_| ParseError { + message: format!("Invalid version number: {}", version_str), + span: ident.span, + })?; + + let span = start.merge(ident.span); + let version = Version::new(number, span); + + // Check for + (at least this version) + if self.check(TokenKind::Plus) { + self.advance(); + Ok(VersionConstraint::AtLeast(version)) + } else { + Ok(VersionConstraint::Exact(version)) + } + } + + /// Parse migrations: from @v1 = { ... }, from @v2 = { ... } + fn parse_migrations(&mut self) -> Result, ParseError> { + let mut migrations = Vec::new(); + + while self.check(TokenKind::From) { + let start = self.current_span(); + self.advance(); // consume 'from' + + let from_version = self.parse_version()?; + + self.expect(TokenKind::Eq)?; + self.skip_newlines(); + + // Parse the migration body (an expression, typically a record literal) + let body = self.parse_expr()?; + + let span = start.merge(body.span()); + migrations.push(Migration { + from_version, + body, + span, + }); + + self.skip_newlines(); + // Optional comma between migrations + if self.check(TokenKind::Comma) { + self.advance(); + } + self.skip_newlines(); + } + + Ok(migrations) + } + + fn parse_variants(&mut self) -> Result, ParseError> { + let mut variants = Vec::new(); + + // Skip leading pipe + if self.check(TokenKind::Pipe) { + self.advance(); + } + + loop { + let start = self.current_span(); + let name = self.parse_ident()?; + + let fields = if self.check(TokenKind::LParen) { + // Tuple variant + self.advance(); + let mut types = Vec::new(); + while !self.check(TokenKind::RParen) { + types.push(self.parse_type()?); + if !self.check(TokenKind::RParen) { + self.expect(TokenKind::Comma)?; + } + } + self.expect(TokenKind::RParen)?; + VariantFields::Tuple(types) + } else if self.check(TokenKind::LBrace) { + // Record variant + self.advance(); + self.skip_newlines(); + let fields = self.parse_record_fields()?; + self.expect(TokenKind::RBrace)?; + VariantFields::Record(fields) + } else { + VariantFields::Unit + }; + + let span = start.merge(self.previous_span()); + variants.push(Variant { name, fields, span }); + + self.skip_newlines(); + if self.check(TokenKind::Pipe) { + self.advance(); + self.skip_newlines(); + } else { + break; + } + } + + Ok(variants) + } + + fn peek_is_variant(&self) -> bool { + matches!(self.peek_kind(), TokenKind::Ident(_)) + } + + /// Parse an expression + fn parse_expr(&mut self) -> Result { + self.parse_pipe_expr() + } + + /// Parse pipe expressions: a |> b |> c + fn parse_pipe_expr(&mut self) -> Result { + let mut expr = self.parse_or_expr()?; + + while self.check(TokenKind::PipeGt) { + let start = expr.span(); + self.advance(); + self.skip_newlines(); + let right = self.parse_or_expr()?; + let span = start.merge(right.span()); + + // Transform a |> f into f(a) + expr = Expr::Call { + func: Box::new(right), + args: vec![expr], + span, + }; + } + + Ok(expr) + } + + /// Parse or expressions: a || b + fn parse_or_expr(&mut self) -> Result { + let mut expr = self.parse_and_expr()?; + + while self.check(TokenKind::Or) { + let start = expr.span(); + self.advance(); + self.skip_newlines(); + let right = self.parse_and_expr()?; + let span = start.merge(right.span()); + expr = Expr::BinaryOp { + op: BinaryOp::Or, + left: Box::new(expr), + right: Box::new(right), + span, + }; + } + + Ok(expr) + } + + /// Parse and expressions: a && b + fn parse_and_expr(&mut self) -> Result { + let mut expr = self.parse_equality_expr()?; + + while self.check(TokenKind::And) { + let start = expr.span(); + self.advance(); + self.skip_newlines(); + let right = self.parse_equality_expr()?; + let span = start.merge(right.span()); + expr = Expr::BinaryOp { + op: BinaryOp::And, + left: Box::new(expr), + right: Box::new(right), + span, + }; + } + + Ok(expr) + } + + /// Parse equality expressions: a == b, a != b + fn parse_equality_expr(&mut self) -> Result { + let mut expr = self.parse_comparison_expr()?; + + loop { + let op = match self.peek_kind() { + TokenKind::EqEq => BinaryOp::Eq, + TokenKind::Ne => BinaryOp::Ne, + _ => break, + }; + + let start = expr.span(); + self.advance(); + self.skip_newlines(); + let right = self.parse_comparison_expr()?; + let span = start.merge(right.span()); + expr = Expr::BinaryOp { + op, + left: Box::new(expr), + right: Box::new(right), + span, + }; + } + + Ok(expr) + } + + /// Parse comparison expressions: a < b, a <= b, etc. + fn parse_comparison_expr(&mut self) -> Result { + let mut expr = self.parse_additive_expr()?; + + loop { + let op = match self.peek_kind() { + TokenKind::Lt => BinaryOp::Lt, + TokenKind::Le => BinaryOp::Le, + TokenKind::Gt => BinaryOp::Gt, + TokenKind::Ge => BinaryOp::Ge, + _ => break, + }; + + let start = expr.span(); + self.advance(); + self.skip_newlines(); + let right = self.parse_additive_expr()?; + let span = start.merge(right.span()); + expr = Expr::BinaryOp { + op, + left: Box::new(expr), + right: Box::new(right), + span, + }; + } + + Ok(expr) + } + + /// Parse additive expressions: a + b, a - b + fn parse_additive_expr(&mut self) -> Result { + let mut expr = self.parse_multiplicative_expr()?; + + loop { + let op = match self.peek_kind() { + TokenKind::Plus => BinaryOp::Add, + TokenKind::Minus => BinaryOp::Sub, + _ => break, + }; + + let start = expr.span(); + self.advance(); + self.skip_newlines(); + let right = self.parse_multiplicative_expr()?; + let span = start.merge(right.span()); + expr = Expr::BinaryOp { + op, + left: Box::new(expr), + right: Box::new(right), + span, + }; + } + + Ok(expr) + } + + /// Parse multiplicative expressions: a * b, a / b, a % b + fn parse_multiplicative_expr(&mut self) -> Result { + let mut expr = self.parse_unary_expr()?; + + loop { + let op = match self.peek_kind() { + TokenKind::Star => BinaryOp::Mul, + TokenKind::Slash => BinaryOp::Div, + TokenKind::Percent => BinaryOp::Mod, + _ => break, + }; + + let start = expr.span(); + self.advance(); + self.skip_newlines(); + let right = self.parse_unary_expr()?; + let span = start.merge(right.span()); + expr = Expr::BinaryOp { + op, + left: Box::new(expr), + right: Box::new(right), + span, + }; + } + + Ok(expr) + } + + /// Parse unary expressions: -a, !a + fn parse_unary_expr(&mut self) -> Result { + let op = match self.peek_kind() { + TokenKind::Minus => UnaryOp::Neg, + TokenKind::Not => UnaryOp::Not, + _ => return self.parse_call_expr(), + }; + + let start = self.current_span(); + self.advance(); + let operand = self.parse_unary_expr()?; + let span = start.merge(operand.span()); + + Ok(Expr::UnaryOp { + op, + operand: Box::new(operand), + span, + }) + } + + /// Parse call and field access expressions + fn parse_call_expr(&mut self) -> Result { + let mut expr = self.parse_primary_expr()?; + + loop { + if self.check(TokenKind::LParen) { + // Function call + let start = expr.span(); + self.advance(); + let args = self.parse_args()?; + self.expect(TokenKind::RParen)?; + let span = start.merge(self.previous_span()); + expr = Expr::Call { + func: Box::new(expr), + args, + span, + }; + } else if self.check(TokenKind::Dot) { + let start = expr.span(); + self.advance(); + let field = self.parse_ident()?; + + // Check if this is an effect operation: Effect.operation(args) + if self.check(TokenKind::LParen) { + if let Expr::Var(effect) = expr { + self.advance(); + let args = self.parse_args()?; + self.expect(TokenKind::RParen)?; + let span = start.merge(self.previous_span()); + expr = Expr::EffectOp { + effect, + operation: field, + args, + span, + }; + continue; + } + } + + let span = start.merge(field.span); + expr = Expr::Field { + object: Box::new(expr), + field, + span, + }; + } else { + break; + } + } + + Ok(expr) + } + + fn parse_args(&mut self) -> Result, ParseError> { + let mut args = Vec::new(); + + while !self.check(TokenKind::RParen) { + args.push(self.parse_expr()?); + if !self.check(TokenKind::RParen) { + self.expect(TokenKind::Comma)?; + } + } + + Ok(args) + } + + /// Parse primary expressions + fn parse_primary_expr(&mut self) -> Result { + let token = self.peek().clone(); + + match &token.kind { + // Literals + TokenKind::Int(n) => { + let n = *n; + self.advance(); + Ok(Expr::Literal(Literal { + kind: LiteralKind::Int(n), + span: token.span, + })) + } + TokenKind::Float(f) => { + let f = *f; + self.advance(); + Ok(Expr::Literal(Literal { + kind: LiteralKind::Float(f), + span: token.span, + })) + } + TokenKind::String(s) => { + let s = s.clone(); + self.advance(); + Ok(Expr::Literal(Literal { + kind: LiteralKind::String(s), + span: token.span, + })) + } + TokenKind::Char(c) => { + let c = *c; + self.advance(); + Ok(Expr::Literal(Literal { + kind: LiteralKind::Char(c), + span: token.span, + })) + } + TokenKind::Bool(b) => { + let b = *b; + self.advance(); + Ok(Expr::Literal(Literal { + kind: LiteralKind::Bool(b), + span: token.span, + })) + } + + // Identifiers + TokenKind::Ident(_) => { + let ident = self.parse_ident()?; + Ok(Expr::Var(ident)) + } + + // Keywords that start expressions + TokenKind::If => self.parse_if_expr(), + TokenKind::Match => self.parse_match_expr(), + TokenKind::Let => self.parse_let_expr(), + TokenKind::Fn => self.parse_lambda_expr(), + TokenKind::Run => self.parse_run_expr(), + TokenKind::Resume => self.parse_resume_expr(), + + // Delimiters + TokenKind::LParen => self.parse_tuple_or_paren_expr(), + TokenKind::LBrace => self.parse_block_or_record_expr(), + TokenKind::LBracket => self.parse_list_expr(), + + _ => Err(self.error(&format!("Unexpected token: {}", token.kind))), + } + } + + fn parse_if_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::If)?; + + let condition = Box::new(self.parse_expr()?); + + self.expect(TokenKind::Then)?; + self.skip_newlines(); + let then_branch = Box::new(self.parse_expr()?); + + self.skip_newlines(); + self.expect(TokenKind::Else)?; + self.skip_newlines(); + let else_branch = Box::new(self.parse_expr()?); + + let span = start.merge(else_branch.span()); + Ok(Expr::If { + condition, + then_branch, + else_branch, + span, + }) + } + + fn parse_match_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Match)?; + + let scrutinee = Box::new(self.parse_expr()?); + + self.expect(TokenKind::LBrace)?; + self.skip_newlines(); + + let mut arms = Vec::new(); + while !self.check(TokenKind::RBrace) { + arms.push(self.parse_match_arm()?); + self.skip_newlines(); + if self.check(TokenKind::Comma) { + self.advance(); + } + self.skip_newlines(); + } + + let end = self.current_span(); + self.expect(TokenKind::RBrace)?; + + Ok(Expr::Match { + scrutinee, + arms, + span: start.merge(end), + }) + } + + fn parse_match_arm(&mut self) -> Result { + let start = self.current_span(); + let pattern = self.parse_pattern()?; + + let guard = if self.check(TokenKind::If) { + self.advance(); + Some(self.parse_expr()?) + } else { + None + }; + + self.expect(TokenKind::Arrow)?; + self.skip_newlines(); + let body = self.parse_expr()?; + + let span = start.merge(body.span()); + Ok(MatchArm { + pattern, + guard, + body, + span, + }) + } + + fn parse_pattern(&mut self) -> Result { + let token = self.peek().clone(); + + match &token.kind { + TokenKind::Underscore => { + self.advance(); + Ok(Pattern::Wildcard(token.span)) + } + TokenKind::Int(n) => { + let n = *n; + self.advance(); + Ok(Pattern::Literal(Literal { + kind: LiteralKind::Int(n), + span: token.span, + })) + } + TokenKind::String(s) => { + let s = s.clone(); + self.advance(); + Ok(Pattern::Literal(Literal { + kind: LiteralKind::String(s), + span: token.span, + })) + } + TokenKind::Bool(b) => { + let b = *b; + self.advance(); + Ok(Pattern::Literal(Literal { + kind: LiteralKind::Bool(b), + span: token.span, + })) + } + TokenKind::Ident(name) => { + // Check if it starts with uppercase (constructor) or lowercase (variable) + if name.chars().next().map_or(false, |c| c.is_uppercase()) { + self.parse_constructor_pattern() + } else { + let ident = self.parse_ident()?; + Ok(Pattern::Var(ident)) + } + } + TokenKind::LParen => self.parse_tuple_pattern(), + TokenKind::LBrace => self.parse_record_pattern(), + _ => Err(self.error("Expected pattern")), + } + } + + fn parse_constructor_pattern(&mut self) -> Result { + let start = self.current_span(); + let name = self.parse_ident()?; + + if self.check(TokenKind::LParen) { + self.advance(); + let mut fields = Vec::new(); + while !self.check(TokenKind::RParen) { + fields.push(self.parse_pattern()?); + if !self.check(TokenKind::RParen) { + self.expect(TokenKind::Comma)?; + } + } + self.expect(TokenKind::RParen)?; + let span = start.merge(self.previous_span()); + Ok(Pattern::Constructor { name, fields, span }) + } else { + let span = name.span; + Ok(Pattern::Constructor { + name, + fields: Vec::new(), + span, + }) + } + } + + fn parse_tuple_pattern(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::LParen)?; + + let mut elements = Vec::new(); + while !self.check(TokenKind::RParen) { + elements.push(self.parse_pattern()?); + if !self.check(TokenKind::RParen) { + self.expect(TokenKind::Comma)?; + } + } + + self.expect(TokenKind::RParen)?; + let span = start.merge(self.previous_span()); + Ok(Pattern::Tuple { elements, span }) + } + + fn parse_record_pattern(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::LBrace)?; + self.skip_newlines(); + + let mut fields = Vec::new(); + while !self.check(TokenKind::RBrace) { + let name = self.parse_ident()?; + let pattern = if self.check(TokenKind::Colon) { + self.advance(); + self.parse_pattern()? + } else { + Pattern::Var(name.clone()) + }; + fields.push((name, pattern)); + + self.skip_newlines(); + if self.check(TokenKind::Comma) { + self.advance(); + } + self.skip_newlines(); + } + + self.expect(TokenKind::RBrace)?; + let span = start.merge(self.previous_span()); + Ok(Pattern::Record { fields, span }) + } + + fn parse_let_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Let)?; + + let name = self.parse_ident()?; + + let typ = if self.check(TokenKind::Colon) { + self.advance(); + Some(self.parse_type()?) + } else { + None + }; + + self.expect(TokenKind::Eq)?; + self.skip_newlines(); + let value = Box::new(self.parse_expr()?); + + // Check for semicolon (let-in style) + self.skip_newlines(); + if self.check(TokenKind::Semi) { + self.advance(); + self.skip_newlines(); + let body = Box::new(self.parse_expr()?); + let span = start.merge(body.span()); + Ok(Expr::Let { + name, + typ, + value, + body, + span, + }) + } else { + // Let as expression (returns unit) + let span = start.merge(value.span()); + let body = Box::new(Expr::Literal(Literal { + kind: LiteralKind::Unit, + span: Span::default(), + })); + Ok(Expr::Let { + name, + typ, + value, + body, + span, + }) + } + } + + fn parse_lambda_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Fn)?; + + self.expect(TokenKind::LParen)?; + let params = self.parse_lambda_params()?; + self.expect(TokenKind::RParen)?; + + let return_type = if self.check(TokenKind::Colon) { + self.advance(); + Some(Box::new(self.parse_type()?)) + } else { + None + }; + + let effects = if self.check(TokenKind::With) { + self.advance(); + self.parse_effect_list()? + } else { + Vec::new() + }; + + self.expect(TokenKind::Arrow)?; + self.skip_newlines(); + let body = Box::new(self.parse_expr()?); + + let span = start.merge(body.span()); + Ok(Expr::Lambda { + params, + return_type, + effects, + body, + span, + }) + } + + fn parse_lambda_params(&mut self) -> Result, ParseError> { + let mut params = Vec::new(); + + while !self.check(TokenKind::RParen) { + let start = self.current_span(); + let name = self.parse_ident()?; + + let typ = if self.check(TokenKind::Colon) { + self.advance(); + self.parse_type()? + } else { + // Infer type later + TypeExpr::Named(Ident::new("_", Span::default())) + }; + + let span = start.merge(self.previous_span()); + params.push(Parameter { name, typ, span }); + + if !self.check(TokenKind::RParen) { + self.expect(TokenKind::Comma)?; + } + } + + Ok(params) + } + + fn parse_run_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Run)?; + + let expr = Box::new(self.parse_call_expr()?); + + self.expect(TokenKind::With)?; + self.expect(TokenKind::LBrace)?; + self.skip_newlines(); + + let mut handlers = Vec::new(); + while !self.check(TokenKind::RBrace) { + let effect = self.parse_ident()?; + self.expect(TokenKind::Eq)?; + let handler = self.parse_expr()?; + handlers.push((effect, handler)); + + self.skip_newlines(); + if self.check(TokenKind::Comma) { + self.advance(); + } + self.skip_newlines(); + } + + let end = self.current_span(); + self.expect(TokenKind::RBrace)?; + + Ok(Expr::Run { + expr, + handlers, + span: start.merge(end), + }) + } + + fn parse_resume_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::Resume)?; + self.expect(TokenKind::LParen)?; + let value = Box::new(self.parse_expr()?); + self.expect(TokenKind::RParen)?; + let span = start.merge(self.previous_span()); + Ok(Expr::Resume { value, span }) + } + + fn parse_tuple_or_paren_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::LParen)?; + + if self.check(TokenKind::RParen) { + self.advance(); + return Ok(Expr::Literal(Literal { + kind: LiteralKind::Unit, + span: start.merge(self.previous_span()), + })); + } + + let first = self.parse_expr()?; + + if self.check(TokenKind::Comma) { + // Tuple + let mut elements = vec![first]; + while self.check(TokenKind::Comma) { + self.advance(); + if self.check(TokenKind::RParen) { + break; + } + elements.push(self.parse_expr()?); + } + self.expect(TokenKind::RParen)?; + let span = start.merge(self.previous_span()); + Ok(Expr::Tuple { elements, span }) + } else { + // Parenthesized expression + self.expect(TokenKind::RParen)?; + Ok(first) + } + } + + fn parse_block_or_record_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::LBrace)?; + self.skip_newlines(); + + // Empty block + if self.check(TokenKind::RBrace) { + self.advance(); + return Ok(Expr::Literal(Literal { + kind: LiteralKind::Unit, + span: start.merge(self.previous_span()), + })); + } + + // Check if it's a record (ident: expr) or block + if matches!(self.peek_kind(), TokenKind::Ident(_)) { + let lookahead = self.tokens.get(self.pos + 1).map(|t| &t.kind); + if matches!(lookahead, Some(TokenKind::Colon)) { + return self.parse_record_expr_rest(start); + } + } + + // It's a block + self.parse_block_rest(start) + } + + fn parse_record_expr_rest(&mut self, start: Span) -> Result { + let mut fields = Vec::new(); + + while !self.check(TokenKind::RBrace) { + let name = self.parse_ident()?; + self.expect(TokenKind::Colon)?; + let value = self.parse_expr()?; + fields.push((name, value)); + + self.skip_newlines(); + if self.check(TokenKind::Comma) { + self.advance(); + } + self.skip_newlines(); + } + + self.expect(TokenKind::RBrace)?; + let span = start.merge(self.previous_span()); + Ok(Expr::Record { fields, span }) + } + + fn parse_block_rest(&mut self, start: Span) -> Result { + let mut statements = Vec::new(); + + loop { + self.skip_newlines(); + + if self.check(TokenKind::RBrace) { + break; + } + + if self.check(TokenKind::Let) { + // Let statement + let let_start = self.current_span(); + self.advance(); + let name = self.parse_ident()?; + + let typ = if self.check(TokenKind::Colon) { + self.advance(); + Some(self.parse_type()?) + } else { + None + }; + + self.expect(TokenKind::Eq)?; + self.skip_newlines(); + let value = self.parse_expr()?; + let span = let_start.merge(value.span()); + + statements.push(Statement::Let { + name, + typ, + value, + span, + }); + } else { + // Expression statement + let expr = self.parse_expr()?; + statements.push(Statement::Expr(expr)); + } + + self.skip_newlines(); + + // Optional semicolon + if self.check(TokenKind::Semi) { + self.advance(); + } + + self.skip_newlines(); + } + + // The last statement is the result + let result = if let Some(Statement::Expr(expr)) = statements.pop() { + Box::new(expr) + } else { + Box::new(Expr::Literal(Literal { + kind: LiteralKind::Unit, + span: Span::default(), + })) + }; + + self.expect(TokenKind::RBrace)?; + let span = start.merge(self.previous_span()); + + Ok(Expr::Block { + statements, + result, + span, + }) + } + + fn parse_list_expr(&mut self) -> Result { + let start = self.current_span(); + self.expect(TokenKind::LBracket)?; + + let mut elements = Vec::new(); + while !self.check(TokenKind::RBracket) { + elements.push(self.parse_expr()?); + if !self.check(TokenKind::RBracket) { + self.expect(TokenKind::Comma)?; + } + } + + self.expect(TokenKind::RBracket)?; + let span = start.merge(self.previous_span()); + Ok(Expr::List { elements, span }) + } + + // Helper methods + + fn parse_ident(&mut self) -> Result { + let token = self.peek().clone(); + match &token.kind { + TokenKind::Ident(name) => { + let name = name.clone(); + self.advance(); + Ok(Ident::new(name, token.span)) + } + _ => Err(self.error("Expected identifier")), + } + } + + fn peek(&self) -> &Token { + self.tokens.get(self.pos).unwrap_or(&self.eof_token) + } + + fn peek_kind(&self) -> TokenKind { + self.peek().kind.clone() + } + + fn peek_next(&self) -> &Token { + self.tokens.get(self.pos + 1).unwrap_or(&self.eof_token) + } + + fn peek_next_kind(&self) -> TokenKind { + self.peek_next().kind.clone() + } + + fn advance(&mut self) -> &Token { + if !self.is_at_end() { + self.pos += 1; + } + self.tokens.get(self.pos - 1).unwrap() + } + + fn check(&self, kind: TokenKind) -> bool { + std::mem::discriminant(&self.peek_kind()) == std::mem::discriminant(&kind) + } + + fn expect(&mut self, kind: TokenKind) -> Result<&Token, ParseError> { + if self.check(kind.clone()) { + Ok(self.advance()) + } else { + Err(self.error(&format!("Expected {}, found {}", kind, self.peek_kind()))) + } + } + + fn is_at_end(&self) -> bool { + matches!(self.peek_kind(), TokenKind::Eof) + } + + fn skip_newlines(&mut self) { + while self.check(TokenKind::Newline) { + self.advance(); + } + } + + fn current_span(&self) -> Span { + self.peek().span + } + + fn previous_span(&self) -> Span { + self.tokens + .get(self.pos.saturating_sub(1)) + .map(|t| t.span) + .unwrap_or_default() + } + + fn error(&self, message: &str) -> ParseError { + ParseError { + message: message.to_string(), + span: self.current_span(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_function() { + let source = "fn add(a: Int, b: Int): Int = a + b"; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.declarations.len(), 1); + } + + #[test] + fn test_parse_effect() { + let source = r#" + effect Console { + fn print(msg: String): Unit + fn read(): String + } + "#; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.declarations.len(), 1); + } + + #[test] + fn test_parse_if_expr() { + let source = "fn max(a: Int, b: Int): Int = if a > b then a else b"; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.declarations.len(), 1); + } + + #[test] + fn test_parse_lambda() { + let source = "let f = fn(x: Int): Int => x + 1"; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.declarations.len(), 1); + } + + #[test] + fn test_parse_import() { + let source = r#" + import std/list + fn foo(): Int = 42 + "#; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.imports.len(), 1); + assert_eq!(program.imports[0].path.segments.len(), 2); + assert_eq!(program.imports[0].path.segments[0].name, "std"); + assert_eq!(program.imports[0].path.segments[1].name, "list"); + assert_eq!(program.declarations.len(), 1); + } + + #[test] + fn test_parse_import_alias() { + let source = "import std/list as L"; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.imports.len(), 1); + assert!(program.imports[0].alias.is_some()); + assert_eq!(program.imports[0].alias.as_ref().unwrap().name, "L"); + } + + #[test] + fn test_parse_import_selective() { + let source = "import std/list.{map, filter, fold}"; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.imports.len(), 1); + assert!(program.imports[0].items.is_some()); + let items = program.imports[0].items.as_ref().unwrap(); + assert_eq!(items.len(), 3); + assert_eq!(items[0].name, "map"); + assert_eq!(items[1].name, "filter"); + assert_eq!(items[2].name, "fold"); + } + + #[test] + fn test_parse_pub_function() { + let source = "pub fn add(a: Int, b: Int): Int = a + b"; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.declarations.len(), 1); + if let Declaration::Function(f) = &program.declarations[0] { + assert_eq!(f.visibility, Visibility::Public); + } else { + panic!("Expected function declaration"); + } + } + + #[test] + fn test_parse_pub_let() { + let source = "pub let x = 42"; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.declarations.len(), 1); + if let Declaration::Let(l) = &program.declarations[0] { + assert_eq!(l.visibility, Visibility::Public); + } else { + panic!("Expected let declaration"); + } + } + + #[test] + fn test_parse_private_by_default() { + let source = "fn foo(): Int = 42"; + let program = Parser::parse_source(source).unwrap(); + if let Declaration::Function(f) = &program.declarations[0] { + assert_eq!(f.visibility, Visibility::Private); + } else { + panic!("Expected function declaration"); + } + } + + // Schema Evolution tests + + #[test] + fn test_parse_versioned_type() { + let source = "type User @v1 { name: String, email: String }"; + let program = Parser::parse_source(source).unwrap(); + assert_eq!(program.declarations.len(), 1); + if let Declaration::Type(t) = &program.declarations[0] { + assert_eq!(t.name.name, "User"); + assert!(t.version.is_some()); + assert_eq!(t.version.unwrap().number, 1); + } else { + panic!("Expected type declaration"); + } + } + + #[test] + fn test_parse_versioned_type_v2() { + let source = "type User @v2 { name: String, email: String, age: Int }"; + let program = Parser::parse_source(source).unwrap(); + if let Declaration::Type(t) = &program.declarations[0] { + assert!(t.version.is_some()); + assert_eq!(t.version.unwrap().number, 2); + } else { + panic!("Expected type declaration"); + } + } + + #[test] + fn test_parse_versioned_type_expr() { + let source = "let user: User @v2 = x"; + let program = Parser::parse_source(source).unwrap(); + if let Declaration::Let(l) = &program.declarations[0] { + if let Some(TypeExpr::Versioned { base, constraint }) = &l.typ { + if let TypeExpr::Named(name) = base.as_ref() { + assert_eq!(name.name, "User"); + } else { + panic!("Expected Named type"); + } + if let VersionConstraint::Exact(v) = constraint { + assert_eq!(v.number, 2); + } else { + panic!("Expected Exact version constraint"); + } + } else { + panic!("Expected Versioned type expression"); + } + } else { + panic!("Expected let declaration"); + } + } + + #[test] + fn test_parse_version_at_least() { + let source = "let user: User @v2+ = x"; + let program = Parser::parse_source(source).unwrap(); + if let Declaration::Let(l) = &program.declarations[0] { + if let Some(TypeExpr::Versioned { constraint, .. }) = &l.typ { + if let VersionConstraint::AtLeast(v) = constraint { + assert_eq!(v.number, 2); + } else { + panic!("Expected AtLeast version constraint"); + } + } else { + panic!("Expected Versioned type expression"); + } + } else { + panic!("Expected let declaration"); + } + } + + #[test] + fn test_parse_version_latest() { + let source = "let user: User @latest = x"; + let program = Parser::parse_source(source).unwrap(); + if let Declaration::Let(l) = &program.declarations[0] { + if let Some(TypeExpr::Versioned { constraint, .. }) = &l.typ { + assert!(matches!(constraint, VersionConstraint::Latest(_))); + } else { + panic!("Expected Versioned type expression"); + } + } else { + panic!("Expected let declaration"); + } + } + + #[test] + fn test_parse_type_with_migration() { + let source = r#" + type User @v2 { + name: String, + email: String, + age: Int, + + from @v1 = { name: old.name, email: old.email, age: 0 } + } + "#; + let program = Parser::parse_source(source).unwrap(); + if let Declaration::Type(t) = &program.declarations[0] { + assert_eq!(t.name.name, "User"); + assert_eq!(t.version.unwrap().number, 2); + assert_eq!(t.migrations.len(), 1); + assert_eq!(t.migrations[0].from_version.number, 1); + } else { + panic!("Expected type declaration"); + } + } +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..1da996f --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,330 @@ +//! Schema Evolution for the Lux language +//! +//! Handles versioned types, compatibility checking, and migrations. + +#![allow(dead_code)] + +use crate::ast::{Migration, RecordField, TypeDecl, TypeDef}; +use std::collections::HashMap; + +/// Describes the compatibility between two versions of a type +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Compatibility { + /// Fully compatible - no changes needed + Compatible, + /// Compatible with auto-migration (e.g., adding optional field with default) + AutoMigrate(Vec), + /// Breaking change - requires explicit migration + Breaking(Vec), +} + +/// An automatic migration step +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AutoMigration { + /// Add a field with a default value + AddFieldWithDefault { field_name: String, default: String }, + /// Widen a numeric type (e.g., Int32 -> Int64) + WidenType { + field_name: String, + from: String, + to: String, + }, +} + +/// A breaking change that requires explicit migration +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BreakingChange { + /// Field was removed + FieldRemoved { field_name: String }, + /// Field was renamed + FieldRenamed { old_name: String, new_name: String }, + /// Field type changed incompatibly + FieldTypeChanged { + field_name: String, + old_type: String, + new_type: String, + }, + /// Required field added without default + RequiredFieldAdded { field_name: String }, +} + +/// Registry of versioned types +#[derive(Debug, Default)] +pub struct SchemaRegistry { + /// Map from type name to versions: TypeName -> (Version -> TypeDef) + versions: HashMap>, +} + +/// A versioned type definition with its migrations +#[derive(Debug, Clone)] +pub struct VersionedTypeDef { + pub version: u32, + pub definition: TypeDef, + pub migrations: Vec, +} + +impl SchemaRegistry { + pub fn new() -> Self { + Self::default() + } + + /// Register a versioned type + pub fn register(&mut self, name: &str, type_decl: &TypeDecl) { + let version = type_decl.version.map(|v| v.number).unwrap_or(1); + + let versioned_def = VersionedTypeDef { + version, + definition: type_decl.definition.clone(), + migrations: type_decl.migrations.clone(), + }; + + self.versions + .entry(name.to_string()) + .or_default() + .insert(version, versioned_def); + } + + /// Get all versions of a type + pub fn get_versions(&self, name: &str) -> Option<&HashMap> { + self.versions.get(name) + } + + /// Get a specific version of a type + pub fn get_version(&self, name: &str, version: u32) -> Option<&VersionedTypeDef> { + self.versions.get(name)?.get(&version) + } + + /// Get the latest version number of a type + pub fn latest_version(&self, name: &str) -> Option { + self.versions.get(name)?.keys().max().copied() + } + + /// Check compatibility between two versions + pub fn check_compatibility( + &self, + name: &str, + from_version: u32, + to_version: u32, + ) -> Result { + let from_def = self + .get_version(name, from_version) + .ok_or_else(|| format!("Version {} of type '{}' not found", from_version, name))?; + let to_def = self + .get_version(name, to_version) + .ok_or_else(|| format!("Version {} of type '{}' not found", to_version, name))?; + + compare_type_defs(&from_def.definition, &to_def.definition) + } + + /// Check if a migration exists for a version transition + pub fn has_migration(&self, name: &str, from_version: u32, to_version: u32) -> bool { + if let Some(to_def) = self.get_version(name, to_version) { + to_def + .migrations + .iter() + .any(|m| m.from_version.number == from_version) + } else { + false + } + } + + /// Get the migration chain from one version to another + pub fn get_migration_chain( + &self, + _name: &str, + from_version: u32, + to_version: u32, + ) -> Result, String> { + if from_version >= to_version { + return Ok(vec![]); + } + + // Simple chain: v1 -> v2 -> v3 -> ... -> vN + let mut chain = Vec::new(); + for v in from_version..to_version { + chain.push((v, v + 1)); + } + + Ok(chain) + } +} + +/// Compare two type definitions for compatibility +fn compare_type_defs(from: &TypeDef, to: &TypeDef) -> Result { + match (from, to) { + (TypeDef::Record(from_fields), TypeDef::Record(to_fields)) => { + compare_record_fields(from_fields, to_fields) + } + (TypeDef::Enum(from_variants), TypeDef::Enum(to_variants)) => { + // For enums, adding variants is compatible, removing is breaking + let from_names: Vec<_> = from_variants.iter().map(|v| &v.name.name).collect(); + let to_names: Vec<_> = to_variants.iter().map(|v| &v.name.name).collect(); + + let removed: Vec<_> = from_names + .iter() + .filter(|n| !to_names.contains(n)) + .collect(); + + if removed.is_empty() { + Ok(Compatibility::Compatible) + } else { + Ok(Compatibility::Breaking( + removed + .iter() + .map(|n| BreakingChange::FieldRemoved { + field_name: n.to_string(), + }) + .collect(), + )) + } + } + (TypeDef::Alias(from_type), TypeDef::Alias(to_type)) => { + // Type aliases: check if the underlying types are compatible + if from_type == to_type { + Ok(Compatibility::Compatible) + } else { + Ok(Compatibility::Breaking(vec![ + BreakingChange::FieldTypeChanged { + field_name: "".to_string(), + old_type: format!("{:?}", from_type), + new_type: format!("{:?}", to_type), + }, + ])) + } + } + _ => { + // Different type kinds are breaking + Ok(Compatibility::Breaking(vec![])) + } + } +} + +/// Compare record fields for compatibility +fn compare_record_fields( + from: &[RecordField], + to: &[RecordField], +) -> Result { + let from_map: HashMap<&str, &RecordField> = + from.iter().map(|f| (f.name.name.as_str(), f)).collect(); + let to_map: HashMap<&str, &RecordField> = + to.iter().map(|f| (f.name.name.as_str(), f)).collect(); + + let mut auto_migrations = Vec::new(); + let mut breaking_changes = Vec::new(); + + // Check for removed fields + for name in from_map.keys() { + if !to_map.contains_key(name) { + breaking_changes.push(BreakingChange::FieldRemoved { + field_name: name.to_string(), + }); + } + } + + // Check for added fields + for (name, field) in &to_map { + if !from_map.contains_key(name) { + // New field - check if it's optional or has a default + // For now, treat all new fields as potentially requiring migration + // In a full implementation, we'd check for Option types or default annotations + if is_optional_type(&field.typ) { + auto_migrations.push(AutoMigration::AddFieldWithDefault { + field_name: name.to_string(), + default: "None".to_string(), + }); + } else { + breaking_changes.push(BreakingChange::RequiredFieldAdded { + field_name: name.to_string(), + }); + } + } + } + + // Check for type changes in existing fields + for (name, from_field) in &from_map { + if let Some(to_field) = to_map.get(name) { + if from_field.typ != to_field.typ { + // Types differ - check if it's a compatible widening + // For now, treat all type changes as breaking + breaking_changes.push(BreakingChange::FieldTypeChanged { + field_name: name.to_string(), + old_type: format!("{:?}", from_field.typ), + new_type: format!("{:?}", to_field.typ), + }); + } + } + } + + if !breaking_changes.is_empty() { + Ok(Compatibility::Breaking(breaking_changes)) + } else if !auto_migrations.is_empty() { + Ok(Compatibility::AutoMigrate(auto_migrations)) + } else { + Ok(Compatibility::Compatible) + } +} + +/// Check if a type expression represents an optional type +fn is_optional_type(typ: &crate::ast::TypeExpr) -> bool { + match typ { + crate::ast::TypeExpr::Named(ident) => ident.name == "Option", + crate::ast::TypeExpr::App(base, _) => { + if let crate::ast::TypeExpr::Named(ident) = base.as_ref() { + ident.name == "Option" + } else { + false + } + } + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{Ident, Span, TypeExpr}; + + fn make_field(name: &str, typ: &str) -> RecordField { + RecordField { + name: Ident::new(name, Span::default()), + typ: TypeExpr::Named(Ident::new(typ, Span::default())), + span: Span::default(), + } + } + + #[test] + fn test_compatible_same_fields() { + let from = vec![make_field("name", "String"), make_field("age", "Int")]; + let to = vec![make_field("name", "String"), make_field("age", "Int")]; + + let result = compare_record_fields(&from, &to).unwrap(); + assert_eq!(result, Compatibility::Compatible); + } + + #[test] + fn test_breaking_field_removed() { + let from = vec![make_field("name", "String"), make_field("age", "Int")]; + let to = vec![make_field("name", "String")]; + + let result = compare_record_fields(&from, &to).unwrap(); + assert!(matches!(result, Compatibility::Breaking(_))); + } + + #[test] + fn test_breaking_field_added_required() { + let from = vec![make_field("name", "String")]; + let to = vec![make_field("name", "String"), make_field("age", "Int")]; + + let result = compare_record_fields(&from, &to).unwrap(); + assert!(matches!(result, Compatibility::Breaking(_))); + } + + #[test] + fn test_breaking_field_type_changed() { + let from = vec![make_field("name", "String")]; + let to = vec![make_field("name", "Int")]; + + let result = compare_record_fields(&from, &to).unwrap(); + assert!(matches!(result, Compatibility::Breaking(_))); + } +} diff --git a/src/typechecker.rs b/src/typechecker.rs new file mode 100644 index 0000000..9ad4f46 --- /dev/null +++ b/src/typechecker.rs @@ -0,0 +1,1228 @@ +//! Type checker for the Lux language + +#![allow(dead_code, unused_variables)] + +use crate::ast::{ + self, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl, Ident, ImportDecl, + LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span, Statement, + TypeDecl, TypeExpr, UnaryOp, VariantFields, +}; +use crate::modules::ModuleLoader; +use crate::types::{ + self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, Type, TypeEnv, TypeScheme, + VariantDef, VariantFieldsDef, +}; + +/// Type checking error +#[derive(Debug, Clone)] +pub struct TypeError { + pub message: String, + pub span: Span, +} + +impl std::fmt::Display for TypeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Type error at {}-{}: {}", + self.span.start, self.span.end, self.message + ) + } +} + +/// Type checker +pub struct TypeChecker { + env: TypeEnv, + current_effects: EffectSet, + errors: Vec, +} + +impl TypeChecker { + pub fn new() -> Self { + Self { + env: TypeEnv::with_builtins(), + current_effects: EffectSet::empty(), + errors: Vec::new(), + } + } + + /// Type check a program + pub fn check_program(&mut self, program: &Program) -> Result<(), Vec> { + // First pass: collect all declarations + for decl in &program.declarations { + self.collect_declaration(decl); + } + + // Second pass: type check all declarations + for decl in &program.declarations { + self.check_declaration(decl); + } + + if self.errors.is_empty() { + Ok(()) + } else { + Err(self.errors.clone()) + } + } + + /// Type check a program with module support + pub fn check_program_with_modules( + &mut self, + program: &Program, + loader: &ModuleLoader, + ) -> Result<(), Vec> { + // First, process imports + self.load_type_imports(&program.imports, loader); + + // Then check the program normally + self.check_program(program) + } + + /// Load type bindings from imported modules + fn load_type_imports(&mut self, imports: &[ImportDecl], loader: &ModuleLoader) { + use crate::modules::ImportKind; + + let resolved = match loader.resolve_imports(imports) { + Ok(r) => r, + Err(e) => { + self.errors.push(TypeError { + message: format!("Import error: {}", e.message), + span: Span { start: 0, end: 0 }, + }); + return; + } + }; + + for (name, import) in resolved { + match import.kind { + ImportKind::Module => { + // Import as a module object - create a record type + let module = match loader.get_module(&import.module_path) { + Some(m) => m, + None => continue, + }; + + // Create a temporary checker to get types from the module + let mut module_checker = TypeChecker::new(); + for decl in &module.program.declarations { + module_checker.collect_declaration(decl); + } + + // Build a record type with all exported names + let mut fields = Vec::new(); + for export_name in &module.exports { + if let Some(scheme) = module_checker.env.lookup(export_name) { + fields.push((export_name.clone(), scheme.instantiate())); + } + } + + if !fields.is_empty() { + self.env.bind(&name, TypeScheme::mono(Type::Record(fields))); + } + } + ImportKind::Direct => { + // Import a specific name directly + let module = match loader.get_module(&import.module_path) { + Some(m) => m, + None => continue, + }; + + // Get the type for this specific export + let mut module_checker = TypeChecker::new(); + for decl in &module.program.declarations { + module_checker.collect_declaration(decl); + } + + if let Some(scheme) = module_checker.env.lookup(&import.name) { + self.env.bind(&name, scheme.clone()); + } + + // Also copy effects if the imported name is an effect + if let Some(effect) = module_checker.env.lookup_effect(&import.name) { + self.env.effects.insert(name.clone(), effect.clone()); + } + + // Also copy types if the imported name is a type + if let Some(type_def) = module_checker.env.types.get(&import.name) { + self.env.types.insert(name.clone(), type_def.clone()); + } + } + } + } + } + + /// Collect type signatures from declarations (first pass) + fn collect_declaration(&mut self, decl: &Declaration) { + match decl { + Declaration::Function(func) => { + let typ = self.function_type(func); + self.env.bind(&func.name.name, TypeScheme::mono(typ)); + } + Declaration::Effect(effect) => { + let effect_def = self.effect_def(effect); + self.env + .effects + .insert(effect.name.name.clone(), effect_def); + } + Declaration::Type(type_decl) => { + let type_def = self.type_def(type_decl); + self.env.types.insert(type_decl.name.name.clone(), type_def); + } + Declaration::Handler(handler) => { + let handler_def = self.handler_def(handler); + self.env + .handlers + .insert(handler.name.name.clone(), handler_def); + } + Declaration::Let(let_decl) => { + // Will be typed in second pass + let typ = if let Some(ref type_expr) = let_decl.typ { + self.resolve_type(type_expr) + } else { + Type::var() + }; + self.env.bind(&let_decl.name.name, TypeScheme::mono(typ)); + } + } + } + + /// Type check a declaration (second pass) + fn check_declaration(&mut self, decl: &Declaration) { + match decl { + Declaration::Function(func) => { + self.check_function(func); + } + Declaration::Let(let_decl) => { + self.check_let_decl(let_decl); + } + Declaration::Handler(handler) => { + self.check_handler(handler); + } + // Effects and types don't need checking beyond collection + _ => {} + } + } + + fn check_function(&mut self, func: &FunctionDecl) { + // Set up the environment with parameters + let mut local_env = self.env.clone(); + for param in &func.params { + let param_type = self.resolve_type(¶m.typ); + local_env.bind(¶m.name.name, TypeScheme::mono(param_type)); + } + + // Set current effects + let old_effects = std::mem::replace( + &mut self.current_effects, + EffectSet::from_iter(func.effects.iter().map(|e| e.name.clone())), + ); + + // Type check the body + let old_env = std::mem::replace(&mut self.env, local_env); + let body_type = self.infer_expr(&func.body); + self.env = old_env; + self.current_effects = old_effects; + + // Check that body type matches return type + let return_type = self.resolve_type(&func.return_type); + if let Err(e) = unify(&body_type, &return_type) { + self.errors.push(TypeError { + message: format!( + "Function '{}' body has type {}, but declared return type is {}: {}", + func.name.name, body_type, return_type, e + ), + span: func.span, + }); + } + } + + fn check_let_decl(&mut self, let_decl: &LetDecl) { + let inferred = self.infer_expr(&let_decl.value); + + if let Some(ref type_expr) = let_decl.typ { + let declared = self.resolve_type(type_expr); + if let Err(e) = unify(&inferred, &declared) { + self.errors.push(TypeError { + message: format!( + "Variable '{}' has type {}, but declared type is {}: {}", + let_decl.name.name, inferred, declared, e + ), + span: let_decl.span, + }); + } + } + + // Update the binding with the inferred type + let scheme = self.env.generalize(&inferred); + self.env.bind(&let_decl.name.name, scheme); + } + + fn check_handler(&mut self, handler: &HandlerDecl) { + // Look up the effect being handled + let effect = match self.env.lookup_effect(&handler.effect.name) { + Some(e) => e.clone(), + None => { + self.errors.push(TypeError { + message: format!("Unknown effect: {}", handler.effect.name), + span: handler.effect.span, + }); + return; + } + }; + + // Check each implementation + for impl_ in &handler.implementations { + let op = match effect + .operations + .iter() + .find(|o| o.name == impl_.op_name.name) + { + Some(o) => o.clone(), + None => { + self.errors.push(TypeError { + message: format!( + "Effect '{}' has no operation '{}'", + handler.effect.name, impl_.op_name.name + ), + span: impl_.op_name.span, + }); + continue; + } + }; + + // Set up environment with operation parameters + let mut local_env = self.env.clone(); + for (i, param_name) in impl_.params.iter().enumerate() { + if i < op.params.len() { + local_env.bind(¶m_name.name, TypeScheme::mono(op.params[i].1.clone())); + } + } + + // Add resume if present + if let Some(ref resume) = impl_.resume { + // resume has type: fn(return_type) -> result_type + let resume_type = Type::function(vec![op.return_type.clone()], Type::var()); + local_env.bind(&resume.name, TypeScheme::mono(resume_type)); + } + + // Type check the implementation body + let old_env = std::mem::replace(&mut self.env, local_env); + let _body_type = self.infer_expr(&impl_.body); + self.env = old_env; + } + } + + /// Infer the type of an expression + fn infer_expr(&mut self, expr: &Expr) -> Type { + match expr { + Expr::Literal(lit) => self.infer_literal(lit), + + Expr::Var(ident) => match self.env.lookup(&ident.name) { + Some(scheme) => scheme.instantiate(), + None => { + self.errors.push(TypeError { + message: format!("Undefined variable: {}", ident.name), + span: ident.span, + }); + Type::Error + } + }, + + Expr::BinaryOp { + op, + left, + right, + span, + } => self.infer_binary_op(*op, left, right, *span), + + Expr::UnaryOp { op, operand, span } => self.infer_unary_op(*op, operand, *span), + + Expr::Call { func, args, span } => self.infer_call(func, args, *span), + + Expr::EffectOp { + effect, + operation, + args, + span, + } => self.infer_effect_op(effect, operation, args, *span), + + Expr::Field { + object, + field, + span, + } => self.infer_field(object, field, *span), + + Expr::Lambda { + params, + return_type, + effects, + body, + span, + } => self.infer_lambda(params, return_type.as_deref(), effects, body, *span), + + Expr::Let { + name, + typ, + value, + body, + span, + } => self.infer_let(name, typ.as_ref(), value, body, *span), + + Expr::If { + condition, + then_branch, + else_branch, + span, + } => self.infer_if(condition, then_branch, else_branch, *span), + + Expr::Match { + scrutinee, + arms, + span, + } => self.infer_match(scrutinee, arms, *span), + + Expr::Block { + statements, + result, + span, + } => self.infer_block(statements, result, *span), + + Expr::Record { fields, span } => self.infer_record(fields, *span), + + Expr::Tuple { elements, span } => self.infer_tuple(elements, *span), + + Expr::List { elements, span } => self.infer_list(elements, *span), + + Expr::Run { + expr, + handlers, + span, + } => self.infer_run(expr, handlers, *span), + + Expr::Resume { value, span } => { + // Resume is special - it continues the computation + // For now, just infer the value type + let _ = self.infer_expr(value); + Type::var() // Resume's type depends on context + } + } + } + + fn infer_literal(&self, lit: &Literal) -> Type { + match &lit.kind { + LiteralKind::Int(_) => Type::Int, + LiteralKind::Float(_) => Type::Float, + LiteralKind::String(_) => Type::String, + LiteralKind::Char(_) => Type::Char, + LiteralKind::Bool(_) => Type::Bool, + LiteralKind::Unit => Type::Unit, + } + } + + fn infer_binary_op(&mut self, op: BinaryOp, left: &Expr, right: &Expr, span: Span) -> Type { + let left_type = self.infer_expr(left); + let right_type = self.infer_expr(right); + + match op { + BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => { + // Arithmetic: both operands must be same numeric type + if let Err(e) = unify(&left_type, &right_type) { + self.errors.push(TypeError { + message: format!("Operands of '{}' must have same type: {}", op, e), + span, + }); + } + // Check that they're numeric + match &left_type { + Type::Int | Type::Float | Type::Var(_) => left_type, + _ => { + self.errors.push(TypeError { + message: format!( + "Operator '{}' requires numeric operands, got {}", + op, left_type + ), + span, + }); + Type::Error + } + } + } + + BinaryOp::Eq | BinaryOp::Ne => { + // Equality: operands must have same type + if let Err(e) = unify(&left_type, &right_type) { + self.errors.push(TypeError { + message: format!("Operands of '{}' must have same type: {}", op, e), + span, + }); + } + Type::Bool + } + + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => { + // Comparison: operands must be same orderable type + if let Err(e) = unify(&left_type, &right_type) { + self.errors.push(TypeError { + message: format!("Operands of '{}' must have same type: {}", op, e), + span, + }); + } + Type::Bool + } + + BinaryOp::And | BinaryOp::Or => { + // Logical: both must be Bool + if let Err(e) = unify(&left_type, &Type::Bool) { + self.errors.push(TypeError { + message: format!("Left operand of '{}' must be Bool: {}", op, e), + span: left.span(), + }); + } + if let Err(e) = unify(&right_type, &Type::Bool) { + self.errors.push(TypeError { + message: format!("Right operand of '{}' must be Bool: {}", op, e), + span: right.span(), + }); + } + Type::Bool + } + + BinaryOp::Pipe => { + // Pipe: a |> f means f(a) + // right must be a function that accepts left's type + let result_type = Type::var(); + let expected_fn = Type::function(vec![left_type.clone()], result_type.clone()); + if let Err(e) = unify(&right_type, &expected_fn) { + self.errors.push(TypeError { + message: format!( + "Pipe target must be a function accepting {}: {}", + left_type, e + ), + span, + }); + } + result_type + } + } + } + + fn infer_unary_op(&mut self, op: UnaryOp, operand: &Expr, span: Span) -> Type { + let operand_type = self.infer_expr(operand); + + match op { + UnaryOp::Neg => match &operand_type { + Type::Int | Type::Float | Type::Var(_) => operand_type, + _ => { + self.errors.push(TypeError { + message: format!( + "Operator '-' requires numeric operand, got {}", + operand_type + ), + span, + }); + Type::Error + } + }, + UnaryOp::Not => { + if let Err(e) = unify(&operand_type, &Type::Bool) { + self.errors.push(TypeError { + message: format!("Operator '!' requires Bool operand: {}", e), + span, + }); + } + Type::Bool + } + } + } + + fn infer_call(&mut self, func: &Expr, args: &[Expr], span: Span) -> Type { + let func_type = self.infer_expr(func); + let arg_types: Vec = args.iter().map(|a| self.infer_expr(a)).collect(); + + let result_type = Type::var(); + let expected_fn = Type::function(arg_types.clone(), result_type.clone()); + + match unify(&func_type, &expected_fn) { + Ok(subst) => result_type.apply(&subst), + Err(e) => { + self.errors.push(TypeError { + message: format!("Type mismatch in function call: {}", e), + span, + }); + Type::Error + } + } + } + + fn infer_effect_op( + &mut self, + effect: &Ident, + operation: &Ident, + args: &[Expr], + span: Span, + ) -> Type { + // Check if this is actually a module access, not an effect operation + // This includes stdlib modules (List, String, etc.) and user-imported modules + if let Some(scheme) = self.env.lookup(&effect.name) { + let module_type = scheme.instantiate(); + // Get the field type - if it's a Record, treat it as a module + if let Type::Record(fields) = &module_type { + if let Some((_, field_type)) = fields.iter().find(|(n, _)| n == &operation.name) { + // It's a function call on a module field + let arg_types: Vec = args.iter().map(|a| self.infer_expr(a)).collect(); + let result_type = Type::var(); + let expected_fn = Type::function(arg_types, result_type.clone()); + + if let Err(e) = unify(field_type, &expected_fn) { + self.errors.push(TypeError { + message: format!( + "Type mismatch in {}.{} call: {}", + effect.name, operation.name, e + ), + span, + }); + } + return result_type; + } else { + self.errors.push(TypeError { + message: format!( + "Module '{}' has no member '{}'", + effect.name, operation.name + ), + span, + }); + return Type::Error; + } + } + // Fall through to normal error handling if module field not found + } + + // Built-in effects are always available + let builtin_effects = ["Console", "Fail", "State"]; + let is_builtin = builtin_effects.contains(&effect.name.as_str()); + + // Check that we're in a context that allows this effect + if !is_builtin && !self.current_effects.contains(&effect.name) { + self.errors.push(TypeError { + message: format!( + "Effect '{}' not available in current context. Available: {{{}}}", + effect.name, self.current_effects + ), + span, + }); + } + + // Look up the operation - clone to avoid borrow issues + let op = self + .env + .lookup_effect_op(&effect.name, &operation.name) + .cloned(); + + match op { + Some(op) => { + // Check argument types + let arg_types: Vec = args.iter().map(|a| self.infer_expr(a)).collect(); + + if arg_types.len() != op.params.len() { + self.errors.push(TypeError { + message: format!( + "Effect operation '{}.{}' expects {} arguments, got {}", + effect.name, + operation.name, + op.params.len(), + arg_types.len() + ), + span, + }); + } + + for (i, (arg_type, (_, param_type))) in + arg_types.iter().zip(op.params.iter()).enumerate() + { + if let Err(e) = unify(arg_type, param_type) { + self.errors.push(TypeError { + message: format!( + "Argument {} of '{}.{}' has type {}, expected {}: {}", + i + 1, + effect.name, + operation.name, + arg_type, + param_type, + e + ), + span, + }); + } + } + + op.return_type.clone() + } + None => { + self.errors.push(TypeError { + message: format!( + "Unknown effect operation: {}.{}", + effect.name, operation.name + ), + span, + }); + Type::Error + } + } + } + + fn infer_field(&mut self, object: &Expr, field: &Ident, span: Span) -> Type { + let object_type = self.infer_expr(object); + + match &object_type { + Type::Record(fields) => match fields.iter().find(|(n, _)| n == &field.name) { + Some((_, t)) => t.clone(), + None => { + self.errors.push(TypeError { + message: format!("Record has no field '{}'", field.name), + span, + }); + Type::Error + } + }, + Type::Var(_) => { + // Can't infer field access on unknown type + Type::var() + } + _ => { + self.errors.push(TypeError { + message: format!( + "Cannot access field '{}' on non-record type {}", + field.name, object_type + ), + span, + }); + Type::Error + } + } + } + + fn infer_lambda( + &mut self, + params: &[Parameter], + return_type: Option<&TypeExpr>, + effects: &[Ident], + body: &Expr, + _span: Span, + ) -> Type { + // Set up environment with parameters + let mut local_env = self.env.clone(); + let param_types: Vec = params + .iter() + .map(|p| { + let t = self.resolve_type(&p.typ); + local_env.bind(&p.name.name, TypeScheme::mono(t.clone())); + t + }) + .collect(); + + // Set current effects + let effect_set = EffectSet::from_iter(effects.iter().map(|e| e.name.clone())); + let old_effects = std::mem::replace(&mut self.current_effects, effect_set.clone()); + + // Type check body + let old_env = std::mem::replace(&mut self.env, local_env); + let body_type = self.infer_expr(body); + self.env = old_env; + self.current_effects = old_effects; + + // Check return type if specified + let ret_type = if let Some(rt) = return_type { + let declared = self.resolve_type(rt); + if let Err(e) = unify(&body_type, &declared) { + self.errors.push(TypeError { + message: format!( + "Lambda body type {} doesn't match declared {}: {}", + body_type, declared, e + ), + span: body.span(), + }); + } + declared + } else { + body_type + }; + + Type::function_with_effects(param_types, ret_type, effect_set) + } + + fn infer_let( + &mut self, + name: &Ident, + typ: Option<&TypeExpr>, + value: &Expr, + body: &Expr, + _span: Span, + ) -> Type { + let value_type = self.infer_expr(value); + + // Check declared type if present + if let Some(type_expr) = typ { + let declared = self.resolve_type(type_expr); + if let Err(e) = unify(&value_type, &declared) { + self.errors.push(TypeError { + message: format!( + "Variable '{}' has type {}, but declared type is {}: {}", + name.name, value_type, declared, e + ), + span: name.span, + }); + } + } + + // Extend environment and check body + let scheme = self.env.generalize(&value_type); + let old_env = self.env.clone(); + self.env.bind(&name.name, scheme); + let body_type = self.infer_expr(body); + self.env = old_env; + + body_type + } + + fn infer_if( + &mut self, + condition: &Expr, + then_branch: &Expr, + else_branch: &Expr, + span: Span, + ) -> Type { + let cond_type = self.infer_expr(condition); + if let Err(e) = unify(&cond_type, &Type::Bool) { + self.errors.push(TypeError { + message: format!("If condition must be Bool, got {}: {}", cond_type, e), + span: condition.span(), + }); + } + + let then_type = self.infer_expr(then_branch); + let else_type = self.infer_expr(else_branch); + + match unify(&then_type, &else_type) { + Ok(subst) => then_type.apply(&subst), + Err(e) => { + self.errors.push(TypeError { + message: format!( + "If branches have incompatible types: then={}, else={}: {}", + then_type, else_type, e + ), + span, + }); + Type::Error + } + } + } + + fn infer_match(&mut self, scrutinee: &Expr, arms: &[MatchArm], span: Span) -> Type { + let scrutinee_type = self.infer_expr(scrutinee); + + if arms.is_empty() { + self.errors.push(TypeError { + message: "Match expression must have at least one arm".to_string(), + span, + }); + return Type::Error; + } + + let mut result_type: Option = None; + + for arm in arms { + // Check pattern and get bindings + let bindings = self.check_pattern(&arm.pattern, &scrutinee_type); + + // Extend environment with pattern bindings + let old_env = self.env.clone(); + for (name, typ) in bindings { + self.env.bind(name, TypeScheme::mono(typ)); + } + + // Check guard if present + if let Some(ref guard) = arm.guard { + let guard_type = self.infer_expr(guard); + if let Err(e) = unify(&guard_type, &Type::Bool) { + self.errors.push(TypeError { + message: format!("Match guard must be Bool: {}", e), + span: guard.span(), + }); + } + } + + // Check body + let body_type = self.infer_expr(&arm.body); + self.env = old_env; + + // Unify with previous arms + match &result_type { + None => result_type = Some(body_type), + Some(prev) => { + if let Err(e) = unify(prev, &body_type) { + self.errors.push(TypeError { + message: format!( + "Match arm has incompatible type: expected {}, got {}: {}", + prev, body_type, e + ), + span: arm.span, + }); + } + } + } + } + + result_type.unwrap_or(Type::Error) + } + + fn check_pattern(&mut self, pattern: &Pattern, expected: &Type) -> Vec<(String, Type)> { + match pattern { + Pattern::Wildcard(_) => Vec::new(), + + Pattern::Var(ident) => { + vec![(ident.name.clone(), expected.clone())] + } + + Pattern::Literal(lit) => { + let lit_type = self.infer_literal(lit); + if let Err(e) = unify(&lit_type, expected) { + self.errors.push(TypeError { + message: format!("Pattern literal type mismatch: {}", e), + span: lit.span, + }); + } + Vec::new() + } + + Pattern::Constructor { name, fields, span } => { + // Look up constructor + // For now, handle Option specially + match name.name.as_str() { + "None" => { + if let Err(e) = unify(expected, &Type::Option(Box::new(Type::var()))) { + self.errors.push(TypeError { + message: format!( + "None pattern doesn't match type {}: {}", + expected, e + ), + span: *span, + }); + } + Vec::new() + } + "Some" => { + let inner_type = Type::var(); + if let Err(e) = unify(expected, &Type::Option(Box::new(inner_type.clone()))) + { + self.errors.push(TypeError { + message: format!( + "Some pattern doesn't match type {}: {}", + expected, e + ), + span: *span, + }); + } + if fields.len() == 1 { + self.check_pattern(&fields[0], &inner_type) + } else { + Vec::new() + } + } + _ => { + // Generic constructor handling + let mut bindings = Vec::new(); + for field in fields { + bindings.extend(self.check_pattern(field, &Type::var())); + } + bindings + } + } + } + + Pattern::Tuple { elements, span } => { + let element_types: Vec = elements.iter().map(|_| Type::var()).collect(); + if let Err(e) = unify(expected, &Type::Tuple(element_types.clone())) { + self.errors.push(TypeError { + message: format!("Tuple pattern doesn't match type {}: {}", expected, e), + span: *span, + }); + } + + let mut bindings = Vec::new(); + for (elem, typ) in elements.iter().zip(element_types.iter()) { + bindings.extend(self.check_pattern(elem, typ)); + } + bindings + } + + Pattern::Record { fields, span } => { + let field_types: Vec<(String, Type)> = fields + .iter() + .map(|(n, _)| (n.name.clone(), Type::var())) + .collect(); + + if let Err(e) = unify(expected, &Type::Record(field_types.clone())) { + self.errors.push(TypeError { + message: format!("Record pattern doesn't match type {}: {}", expected, e), + span: *span, + }); + } + + let mut bindings = Vec::new(); + for ((_, pattern), (_, typ)) in fields.iter().zip(field_types.iter()) { + bindings.extend(self.check_pattern(pattern, typ)); + } + bindings + } + } + } + + fn infer_block(&mut self, statements: &[Statement], result: &Expr, _span: Span) -> Type { + // Process statements + for stmt in statements { + match stmt { + Statement::Expr(e) => { + self.infer_expr(e); + } + Statement::Let { + name, typ, value, .. + } => { + let value_type = self.infer_expr(value); + + if let Some(type_expr) = typ { + let declared = self.resolve_type(type_expr); + if let Err(e) = unify(&value_type, &declared) { + self.errors.push(TypeError { + message: format!( + "Variable '{}' has type {}, but declared type is {}: {}", + name.name, value_type, declared, e + ), + span: name.span, + }); + } + } + + let scheme = self.env.generalize(&value_type); + self.env.bind(&name.name, scheme); + } + } + } + + // Return the type of the result expression + self.infer_expr(result) + } + + fn infer_record(&mut self, fields: &[(Ident, Expr)], _span: Span) -> Type { + let field_types: Vec<(String, Type)> = fields + .iter() + .map(|(name, expr)| (name.name.clone(), self.infer_expr(expr))) + .collect(); + + Type::Record(field_types) + } + + fn infer_tuple(&mut self, elements: &[Expr], _span: Span) -> Type { + let element_types: Vec = elements.iter().map(|e| self.infer_expr(e)).collect(); + Type::Tuple(element_types) + } + + fn infer_list(&mut self, elements: &[Expr], span: Span) -> Type { + if elements.is_empty() { + return Type::List(Box::new(Type::var())); + } + + let first_type = self.infer_expr(&elements[0]); + for elem in &elements[1..] { + let elem_type = self.infer_expr(elem); + if let Err(e) = unify(&first_type, &elem_type) { + self.errors.push(TypeError { + message: format!("List elements must have same type: {}", e), + span, + }); + } + } + + Type::List(Box::new(first_type)) + } + + fn infer_run(&mut self, expr: &Expr, handlers: &[(Ident, Expr)], _span: Span) -> Type { + // The handlers provide implementations for effects + // After running, those effects are no longer present + + // Add the handled effects to available effects + let handled_effects: EffectSet = + EffectSet::from_iter(handlers.iter().map(|(e, _)| e.name.clone())); + + // Extend current effects with handled ones + let combined = self.current_effects.union(&handled_effects); + let old_effects = std::mem::replace(&mut self.current_effects, combined); + + // Type check the expression + let result_type = self.infer_expr(expr); + + // Type check handlers + for (effect_name, handler_expr) in handlers { + // Just check the handler expression for now + let _ = self.infer_expr(handler_expr); + + // Verify the effect exists + if self.env.lookup_effect(&effect_name.name).is_none() { + self.errors.push(TypeError { + message: format!("Unknown effect: {}", effect_name.name), + span: effect_name.span, + }); + } + } + + self.current_effects = old_effects; + result_type + } + + // Helper methods + + fn function_type(&self, func: &FunctionDecl) -> Type { + let param_types: Vec = func + .params + .iter() + .map(|p| self.resolve_type(&p.typ)) + .collect(); + + let return_type = self.resolve_type(&func.return_type); + let effects = EffectSet::from_iter(func.effects.iter().map(|e| e.name.clone())); + + Type::function_with_effects(param_types, return_type, effects) + } + + fn effect_def(&self, effect: &EffectDecl) -> EffectDef { + EffectDef { + name: effect.name.name.clone(), + type_params: effect.type_params.iter().map(|p| p.name.clone()).collect(), + operations: effect + .operations + .iter() + .map(|op| EffectOpDef { + name: op.name.name.clone(), + params: op + .params + .iter() + .map(|p| (p.name.name.clone(), self.resolve_type(&p.typ))) + .collect(), + return_type: self.resolve_type(&op.return_type), + }) + .collect(), + } + } + + fn type_def(&self, type_decl: &TypeDecl) -> types::TypeDef { + match &type_decl.definition { + ast::TypeDef::Alias(t) => types::TypeDef::Alias(self.resolve_type(t)), + ast::TypeDef::Record(fields) => types::TypeDef::Record( + fields + .iter() + .map(|f| (f.name.name.clone(), self.resolve_type(&f.typ))) + .collect(), + ), + ast::TypeDef::Enum(variants) => types::TypeDef::Enum( + variants + .iter() + .map(|v| VariantDef { + name: v.name.name.clone(), + fields: match &v.fields { + VariantFields::Unit => VariantFieldsDef::Unit, + VariantFields::Tuple(types) => VariantFieldsDef::Tuple( + types.iter().map(|t| self.resolve_type(t)).collect(), + ), + VariantFields::Record(fields) => VariantFieldsDef::Record( + fields + .iter() + .map(|f| (f.name.name.clone(), self.resolve_type(&f.typ))) + .collect(), + ), + }, + }) + .collect(), + ), + } + } + + fn handler_def(&self, handler: &HandlerDecl) -> HandlerDef { + HandlerDef { + name: handler.name.name.clone(), + effect: handler.effect.name.clone(), + params: handler + .params + .iter() + .map(|p| (p.name.name.clone(), self.resolve_type(&p.typ))) + .collect(), + } + } + + fn resolve_type(&self, type_expr: &TypeExpr) -> Type { + match type_expr { + TypeExpr::Named(ident) => match ident.name.as_str() { + "Int" => Type::Int, + "Float" => Type::Float, + "Bool" => Type::Bool, + "String" => Type::String, + "Char" => Type::Char, + "Unit" => Type::Unit, + "_" => Type::var(), + name => Type::Named(name.to_string()), + }, + TypeExpr::App(constructor, args) => { + let resolved_args: Vec = args.iter().map(|a| self.resolve_type(a)).collect(); + + // Handle built-in generic types + if let TypeExpr::Named(name) = constructor.as_ref() { + match name.name.as_str() { + "List" if resolved_args.len() == 1 => { + return Type::List(Box::new(resolved_args[0].clone())); + } + "Option" if resolved_args.len() == 1 => { + return Type::Option(Box::new(resolved_args[0].clone())); + } + _ => {} + } + } + + Type::App { + constructor: Box::new(self.resolve_type(constructor)), + args: resolved_args, + } + } + TypeExpr::Function { + params, + return_type, + effects, + } => Type::function_with_effects( + params.iter().map(|p| self.resolve_type(p)).collect(), + self.resolve_type(return_type), + EffectSet::from_iter(effects.iter().map(|e| e.name.clone())), + ), + TypeExpr::Tuple(elements) => { + Type::Tuple(elements.iter().map(|e| self.resolve_type(e)).collect()) + } + TypeExpr::Record(fields) => Type::Record( + fields + .iter() + .map(|f| (f.name.name.clone(), self.resolve_type(&f.typ))) + .collect(), + ), + TypeExpr::Unit => Type::Unit, + TypeExpr::Versioned { + base, + constraint: _, + } => { + // For now, resolve the base type and ignore versioning + // Full version tracking will be added in the type system + self.resolve_type(base) + } + } + } +} + +impl Default for TypeChecker { + fn default() -> Self { + Self::new() + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..6be28cf --- /dev/null +++ b/src/types.rs @@ -0,0 +1,1083 @@ +//! Type system for the Lux language + +#![allow(dead_code)] + +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// Unique ID for type variables +static NEXT_TYPE_VAR: AtomicUsize = AtomicUsize::new(0); + +fn fresh_type_var() -> usize { + NEXT_TYPE_VAR.fetch_add(1, Ordering::SeqCst) +} + +/// Internal type representation +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Type { + /// Type variable (for inference) + Var(usize), + /// Primitive types + Int, + Float, + Bool, + String, + Char, + Unit, + /// Function type with effects + Function { + params: Vec, + return_type: Box, + effects: EffectSet, + }, + /// Generic type application: List, Option + App { + constructor: Box, + args: Vec, + }, + /// Named type (user-defined or built-in) + Named(String), + /// Tuple type + Tuple(Vec), + /// Record type + Record(Vec<(String, Type)>), + /// List type (sugar for App(List, [T])) + List(Box), + /// Option type (sugar for App(Option, [T])) + Option(Box), + /// Versioned type (e.g., User @v2) + Versioned { + base: Box, + version: VersionInfo, + }, + /// Error type (for type errors that shouldn't halt compilation) + Error, +} + +/// Version information for a versioned type +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VersionInfo { + /// Exactly this version + Exact(u32), + /// This version or later + AtLeast(u32), + /// Latest available version + Latest, +} + +impl Type { + pub fn function(params: Vec, return_type: Type) -> Self { + Type::Function { + params, + return_type: Box::new(return_type), + effects: EffectSet::empty(), + } + } + + pub fn function_with_effects(params: Vec, return_type: Type, effects: EffectSet) -> Self { + Type::Function { + params, + return_type: Box::new(return_type), + effects, + } + } + + pub fn var() -> Self { + Type::Var(fresh_type_var()) + } + + /// Check if this type contains a type variable + pub fn contains_var(&self, var: usize) -> bool { + match self { + Type::Var(v) => *v == var, + Type::Function { + params, + return_type, + .. + } => params.iter().any(|p| p.contains_var(var)) || return_type.contains_var(var), + Type::App { constructor, args } => { + constructor.contains_var(var) || args.iter().any(|a| a.contains_var(var)) + } + Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)), + Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)), + Type::List(inner) | Type::Option(inner) => inner.contains_var(var), + Type::Versioned { base, .. } => base.contains_var(var), + _ => false, + } + } + + /// Apply a substitution to this type + pub fn apply(&self, subst: &Substitution) -> Type { + match self { + Type::Var(v) => { + if let Some(t) = subst.get(*v) { + t.apply(subst) + } else { + self.clone() + } + } + Type::Function { + params, + return_type, + effects, + } => Type::Function { + params: params.iter().map(|p| p.apply(subst)).collect(), + return_type: Box::new(return_type.apply(subst)), + effects: effects.clone(), + }, + Type::App { constructor, args } => Type::App { + constructor: Box::new(constructor.apply(subst)), + args: args.iter().map(|a| a.apply(subst)).collect(), + }, + Type::Tuple(elements) => Type::Tuple(elements.iter().map(|e| e.apply(subst)).collect()), + Type::Record(fields) => Type::Record( + fields + .iter() + .map(|(n, t)| (n.clone(), t.apply(subst))) + .collect(), + ), + Type::List(inner) => Type::List(Box::new(inner.apply(subst))), + Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))), + Type::Versioned { base, version } => Type::Versioned { + base: Box::new(base.apply(subst)), + version: version.clone(), + }, + _ => self.clone(), + } + } + + /// Get all free type variables in this type + pub fn free_vars(&self) -> HashSet { + match self { + Type::Var(v) => { + let mut set = HashSet::new(); + set.insert(*v); + set + } + Type::Function { + params, + return_type, + .. + } => { + let mut vars = HashSet::new(); + for p in params { + vars.extend(p.free_vars()); + } + vars.extend(return_type.free_vars()); + vars + } + Type::App { constructor, args } => { + let mut vars = constructor.free_vars(); + for a in args { + vars.extend(a.free_vars()); + } + vars + } + Type::Tuple(elements) => { + let mut vars = HashSet::new(); + for e in elements { + vars.extend(e.free_vars()); + } + vars + } + Type::Record(fields) => { + let mut vars = HashSet::new(); + for (_, t) in fields { + vars.extend(t.free_vars()); + } + vars + } + Type::List(inner) | Type::Option(inner) => inner.free_vars(), + Type::Versioned { base, .. } => base.free_vars(), + _ => HashSet::new(), + } + } +} + +impl fmt::Display for Type { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Type::Var(v) => write!(f, "?{}", v), + Type::Int => write!(f, "Int"), + Type::Float => write!(f, "Float"), + Type::Bool => write!(f, "Bool"), + Type::String => write!(f, "String"), + Type::Char => write!(f, "Char"), + Type::Unit => write!(f, "Unit"), + Type::Function { + params, + return_type, + effects, + } => { + write!(f, "fn(")?; + for (i, p) in params.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", p)?; + } + write!(f, "): {}", return_type)?; + if !effects.is_empty() { + write!(f, " with {{{}}}", effects)?; + } + Ok(()) + } + Type::App { constructor, args } => { + write!(f, "{}<", constructor)?; + for (i, a) in args.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", a)?; + } + write!(f, ">") + } + Type::Named(name) => write!(f, "{}", name), + Type::Tuple(elements) => { + write!(f, "(")?; + for (i, e) in elements.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", e)?; + } + write!(f, ")") + } + Type::Record(fields) => { + write!(f, "{{ ")?; + for (i, (name, typ)) in fields.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", name, typ)?; + } + write!(f, " }}") + } + Type::List(inner) => write!(f, "List<{}>", inner), + Type::Option(inner) => write!(f, "Option<{}>", inner), + Type::Versioned { base, version } => { + write!(f, "{} {}", base, version) + } + Type::Error => write!(f, ""), + } + } +} + +impl fmt::Display for VersionInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VersionInfo::Exact(v) => write!(f, "@v{}", v), + VersionInfo::AtLeast(v) => write!(f, "@v{}+", v), + VersionInfo::Latest => write!(f, "@latest"), + } + } +} + +/// A set of effects +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct EffectSet { + pub effects: HashSet, +} + +impl EffectSet { + pub fn empty() -> Self { + Self { + effects: HashSet::new(), + } + } + + pub fn single(effect: impl Into) -> Self { + let mut effects = HashSet::new(); + effects.insert(effect.into()); + Self { effects } + } + + pub fn from_iter(iter: impl IntoIterator) -> Self { + Self { + effects: iter.into_iter().collect(), + } + } + + pub fn is_empty(&self) -> bool { + self.effects.is_empty() + } + + pub fn contains(&self, effect: &str) -> bool { + self.effects.contains(effect) + } + + pub fn union(&self, other: &EffectSet) -> EffectSet { + EffectSet { + effects: self.effects.union(&other.effects).cloned().collect(), + } + } + + pub fn is_subset(&self, other: &EffectSet) -> bool { + self.effects.is_subset(&other.effects) + } + + pub fn insert(&mut self, effect: impl Into) { + self.effects.insert(effect.into()); + } +} + +impl fmt::Display for EffectSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let effects: Vec<_> = self.effects.iter().collect(); + for (i, e) in effects.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", e)?; + } + Ok(()) + } +} + +/// Type substitution (mapping from type variables to types) +#[derive(Debug, Clone, Default)] +pub struct Substitution { + mapping: HashMap, +} + +impl Substitution { + pub fn new() -> Self { + Self { + mapping: HashMap::new(), + } + } + + pub fn get(&self, var: usize) -> Option<&Type> { + self.mapping.get(&var) + } + + pub fn insert(&mut self, var: usize, typ: Type) { + self.mapping.insert(var, typ); + } + + pub fn compose(&self, other: &Substitution) -> Substitution { + let mut result = Substitution::new(); + + // Apply self to all mappings in other + for (var, typ) in &other.mapping { + result.insert(*var, typ.apply(self)); + } + + // Add mappings from self that aren't in other + for (var, typ) in &self.mapping { + if !result.mapping.contains_key(var) { + result.insert(*var, typ.clone()); + } + } + + result + } +} + +/// Type scheme (polymorphic type) +#[derive(Debug, Clone)] +pub struct TypeScheme { + pub vars: Vec, + pub typ: Type, +} + +impl TypeScheme { + pub fn mono(typ: Type) -> Self { + Self { + vars: Vec::new(), + typ, + } + } + + pub fn poly(vars: Vec, typ: Type) -> Self { + Self { vars, typ } + } + + /// Instantiate this scheme with fresh type variables + pub fn instantiate(&self) -> Type { + let mut subst = Substitution::new(); + for &var in &self.vars { + subst.insert(var, Type::var()); + } + self.typ.apply(&subst) + } + + pub fn free_vars(&self) -> HashSet { + let mut vars = self.typ.free_vars(); + for v in &self.vars { + vars.remove(v); + } + vars + } +} + +impl fmt::Display for TypeScheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.vars.is_empty() { + write!(f, "{}", self.typ) + } else { + write!(f, "forall ")?; + for (i, v) in self.vars.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "?{}", v)?; + } + write!(f, ". {}", self.typ) + } + } +} + +/// Effect definition +#[derive(Debug, Clone)] +pub struct EffectDef { + pub name: String, + pub type_params: Vec, + pub operations: Vec, +} + +/// Effect operation definition +#[derive(Debug, Clone)] +pub struct EffectOpDef { + pub name: String, + pub params: Vec<(String, Type)>, + pub return_type: Type, +} + +/// Type environment +#[derive(Debug, Clone, Default)] +pub struct TypeEnv { + /// Variable bindings + pub bindings: HashMap, + /// Type definitions + pub types: HashMap, + /// Effect definitions + pub effects: HashMap, + /// Handler types + pub handlers: HashMap, +} + +/// Type definition +#[derive(Debug, Clone)] +pub enum TypeDef { + Alias(Type), + Record(Vec<(String, Type)>), + Enum(Vec), +} + +/// Variant definition +#[derive(Debug, Clone)] +pub struct VariantDef { + pub name: String, + pub fields: VariantFieldsDef, +} + +/// Variant fields +#[derive(Debug, Clone)] +pub enum VariantFieldsDef { + Unit, + Tuple(Vec), + Record(Vec<(String, Type)>), +} + +/// Handler definition +#[derive(Debug, Clone)] +pub struct HandlerDef { + pub name: String, + pub effect: String, + pub params: Vec<(String, Type)>, +} + +impl TypeEnv { + pub fn new() -> Self { + Self::default() + } + + /// Create environment with built-in types + pub fn with_builtins() -> Self { + let mut env = Self::new(); + + // Add built-in types + env.types + .insert("Int".to_string(), TypeDef::Alias(Type::Int)); + env.types + .insert("Float".to_string(), TypeDef::Alias(Type::Float)); + env.types + .insert("Bool".to_string(), TypeDef::Alias(Type::Bool)); + env.types + .insert("String".to_string(), TypeDef::Alias(Type::String)); + env.types + .insert("Char".to_string(), TypeDef::Alias(Type::Char)); + env.types + .insert("Unit".to_string(), TypeDef::Alias(Type::Unit)); + + // Add Option type + env.types.insert( + "Option".to_string(), + TypeDef::Enum(vec![ + VariantDef { + name: "None".to_string(), + fields: VariantFieldsDef::Unit, + }, + VariantDef { + name: "Some".to_string(), + fields: VariantFieldsDef::Tuple(vec![Type::Var(0)]), + }, + ]), + ); + + // Add Result type + env.types.insert( + "Result".to_string(), + TypeDef::Enum(vec![ + VariantDef { + name: "Ok".to_string(), + fields: VariantFieldsDef::Tuple(vec![Type::Var(0)]), + }, + VariantDef { + name: "Err".to_string(), + fields: VariantFieldsDef::Tuple(vec![Type::Var(1)]), + }, + ]), + ); + + // Add Console effect + env.effects.insert( + "Console".to_string(), + EffectDef { + name: "Console".to_string(), + type_params: Vec::new(), + operations: vec![ + EffectOpDef { + name: "print".to_string(), + params: vec![("msg".to_string(), Type::String)], + return_type: Type::Unit, + }, + EffectOpDef { + name: "read".to_string(), + params: Vec::new(), + return_type: Type::String, + }, + ], + }, + ); + + // Add Fail effect + env.effects.insert( + "Fail".to_string(), + EffectDef { + name: "Fail".to_string(), + type_params: Vec::new(), + operations: vec![EffectOpDef { + name: "fail".to_string(), + params: vec![("msg".to_string(), Type::String)], + return_type: Type::Var(0), // Returns any type (never returns) + }], + }, + ); + + // Add State effect + env.effects.insert( + "State".to_string(), + EffectDef { + name: "State".to_string(), + type_params: vec!["S".to_string()], + operations: vec![ + EffectOpDef { + name: "get".to_string(), + params: Vec::new(), + return_type: Type::Var(0), // S + }, + EffectOpDef { + name: "put".to_string(), + params: vec![("value".to_string(), Type::Var(0))], // S + return_type: Type::Unit, + }, + ], + }, + ); + + // Add Some and Ok, Err constructors + // Some : fn(a) -> Option + let a = Type::var(); + env.bind( + "Some", + TypeScheme::mono(Type::function(vec![a.clone()], Type::Option(Box::new(a)))), + ); + + // None : Option + env.bind( + "None", + TypeScheme::mono(Type::Option(Box::new(Type::var()))), + ); + + // Ok : fn(a) -> Result + let a = Type::var(); + let e = Type::var(); + env.bind( + "Ok", + TypeScheme::mono(Type::function( + vec![a.clone()], + Type::App { + constructor: Box::new(Type::Named("Result".to_string())), + args: vec![a, e], + }, + )), + ); + + // Err : fn(e) -> Result + let a = Type::var(); + let e = Type::var(); + env.bind( + "Err", + TypeScheme::mono(Type::function( + vec![e.clone()], + Type::App { + constructor: Box::new(Type::Named("Result".to_string())), + args: vec![a, e], + }, + )), + ); + + // Add stdlib modules as record types + // List module + let list_module_type = Type::Record(vec![ + ( + "map".to_string(), + Type::function( + vec![ + Type::List(Box::new(Type::var())), + Type::function(vec![Type::var()], Type::var()), + ], + Type::List(Box::new(Type::var())), + ), + ), + ( + "filter".to_string(), + Type::function( + vec![ + Type::List(Box::new(Type::var())), + Type::function(vec![Type::var()], Type::Bool), + ], + Type::List(Box::new(Type::var())), + ), + ), + ( + "fold".to_string(), + Type::function( + vec![ + Type::List(Box::new(Type::var())), + Type::var(), + Type::function(vec![Type::var(), Type::var()], Type::var()), + ], + Type::var(), + ), + ), + ( + "head".to_string(), + Type::function( + vec![Type::List(Box::new(Type::var()))], + Type::Option(Box::new(Type::var())), + ), + ), + ( + "tail".to_string(), + Type::function( + vec![Type::List(Box::new(Type::var()))], + Type::Option(Box::new(Type::List(Box::new(Type::var())))), + ), + ), + ( + "concat".to_string(), + Type::function( + vec![ + Type::List(Box::new(Type::var())), + Type::List(Box::new(Type::var())), + ], + Type::List(Box::new(Type::var())), + ), + ), + ( + "reverse".to_string(), + Type::function( + vec![Type::List(Box::new(Type::var()))], + Type::List(Box::new(Type::var())), + ), + ), + ( + "length".to_string(), + Type::function(vec![Type::List(Box::new(Type::var()))], Type::Int), + ), + ( + "get".to_string(), + Type::function( + vec![Type::List(Box::new(Type::var())), Type::Int], + Type::Option(Box::new(Type::var())), + ), + ), + ( + "range".to_string(), + Type::function(vec![Type::Int, Type::Int], Type::List(Box::new(Type::Int))), + ), + ]); + env.bind("List", TypeScheme::mono(list_module_type)); + + // String module + let string_module_type = Type::Record(vec![ + ( + "split".to_string(), + Type::function( + vec![Type::String, Type::String], + Type::List(Box::new(Type::String)), + ), + ), + ( + "join".to_string(), + Type::function( + vec![Type::List(Box::new(Type::String)), Type::String], + Type::String, + ), + ), + ( + "trim".to_string(), + Type::function(vec![Type::String], Type::String), + ), + ( + "contains".to_string(), + Type::function(vec![Type::String, Type::String], Type::Bool), + ), + ( + "replace".to_string(), + Type::function(vec![Type::String, Type::String, Type::String], Type::String), + ), + ( + "length".to_string(), + Type::function(vec![Type::String], Type::Int), + ), + ( + "chars".to_string(), + Type::function(vec![Type::String], Type::List(Box::new(Type::Char))), + ), + ( + "lines".to_string(), + Type::function(vec![Type::String], Type::List(Box::new(Type::String))), + ), + ]); + env.bind("String", TypeScheme::mono(string_module_type)); + + // Option module + let option_module_type = Type::Record(vec![ + ( + "map".to_string(), + Type::function( + vec![ + Type::Option(Box::new(Type::var())), + Type::function(vec![Type::var()], Type::var()), + ], + Type::Option(Box::new(Type::var())), + ), + ), + ( + "flatMap".to_string(), + Type::function( + vec![ + Type::Option(Box::new(Type::var())), + Type::function(vec![Type::var()], Type::Option(Box::new(Type::var()))), + ], + Type::Option(Box::new(Type::var())), + ), + ), + ( + "getOrElse".to_string(), + Type::function( + vec![Type::Option(Box::new(Type::var())), Type::var()], + Type::var(), + ), + ), + ( + "isSome".to_string(), + Type::function(vec![Type::Option(Box::new(Type::var()))], Type::Bool), + ), + ( + "isNone".to_string(), + Type::function(vec![Type::Option(Box::new(Type::var()))], Type::Bool), + ), + ]); + env.bind("Option", TypeScheme::mono(option_module_type)); + + // Result module + let result_type = Type::App { + constructor: Box::new(Type::Named("Result".to_string())), + args: vec![Type::var(), Type::var()], + }; + let result_module_type = Type::Record(vec![ + ( + "map".to_string(), + Type::function( + vec![ + result_type.clone(), + Type::function(vec![Type::var()], Type::var()), + ], + result_type.clone(), + ), + ), + ( + "flatMap".to_string(), + Type::function( + vec![ + result_type.clone(), + Type::function(vec![Type::var()], result_type.clone()), + ], + result_type.clone(), + ), + ), + ( + "getOrElse".to_string(), + Type::function(vec![result_type.clone(), Type::var()], Type::var()), + ), + ( + "isOk".to_string(), + Type::function(vec![result_type.clone()], Type::Bool), + ), + ( + "isErr".to_string(), + Type::function(vec![result_type], Type::Bool), + ), + ]); + env.bind("Result", TypeScheme::mono(result_module_type)); + + // Utility functions + env.bind( + "print", + TypeScheme::mono(Type::function(vec![Type::var()], Type::Unit)), + ); + env.bind( + "toString", + TypeScheme::mono(Type::function(vec![Type::var()], Type::String)), + ); + env.bind( + "typeOf", + TypeScheme::mono(Type::function(vec![Type::var()], Type::String)), + ); + + env + } + + pub fn lookup(&self, name: &str) -> Option<&TypeScheme> { + self.bindings.get(name) + } + + pub fn bind(&mut self, name: impl Into, scheme: TypeScheme) { + self.bindings.insert(name.into(), scheme); + } + + pub fn extend(&self, name: impl Into, scheme: TypeScheme) -> TypeEnv { + let mut env = self.clone(); + env.bind(name, scheme); + env + } + + pub fn lookup_effect(&self, name: &str) -> Option<&EffectDef> { + self.effects.get(name) + } + + pub fn lookup_effect_op(&self, effect: &str, op: &str) -> Option<&EffectOpDef> { + self.effects + .get(effect)? + .operations + .iter() + .find(|o| o.name == op) + } + + /// Generalize a type to a type scheme + pub fn generalize(&self, typ: &Type) -> TypeScheme { + let env_vars: HashSet = self.bindings.values().flat_map(|s| s.free_vars()).collect(); + + let type_vars: Vec = typ + .free_vars() + .into_iter() + .filter(|v| !env_vars.contains(v)) + .collect(); + + TypeScheme::poly(type_vars, typ.clone()) + } +} + +/// Unification of types +pub fn unify(t1: &Type, t2: &Type) -> Result { + match (t1, t2) { + // Same type + (Type::Int, Type::Int) + | (Type::Float, Type::Float) + | (Type::Bool, Type::Bool) + | (Type::String, Type::String) + | (Type::Char, Type::Char) + | (Type::Unit, Type::Unit) + | (Type::Error, _) + | (_, Type::Error) => Ok(Substitution::new()), + + // Named types + (Type::Named(a), Type::Named(b)) if a == b => Ok(Substitution::new()), + + // Type variables + (Type::Var(v), t) | (t, Type::Var(v)) => { + if let Type::Var(v2) = t { + if v == v2 { + return Ok(Substitution::new()); + } + } + if t.contains_var(*v) { + return Err(format!("Occurs check failed: ?{} occurs in {}", v, t)); + } + let mut subst = Substitution::new(); + subst.insert(*v, t.clone()); + Ok(subst) + } + + // Function types + ( + Type::Function { + params: p1, + return_type: r1, + effects: e1, + }, + Type::Function { + params: p2, + return_type: r2, + effects: e2, + }, + ) => { + if p1.len() != p2.len() { + return Err(format!( + "Function arity mismatch: expected {} params, got {}", + p1.len(), + p2.len() + )); + } + + // For now, effects must match exactly + if e1 != e2 { + return Err(format!( + "Effect mismatch: expected {{{}}}, got {{{}}}", + e1, e2 + )); + } + + let mut subst = Substitution::new(); + for (a, b) in p1.iter().zip(p2.iter()) { + let s = unify(&a.apply(&subst), &b.apply(&subst))?; + subst = subst.compose(&s); + } + let s = unify(&r1.apply(&subst), &r2.apply(&subst))?; + Ok(subst.compose(&s)) + } + + // Type applications + ( + Type::App { + constructor: c1, + args: a1, + }, + Type::App { + constructor: c2, + args: a2, + }, + ) => { + if a1.len() != a2.len() { + return Err(format!( + "Type argument count mismatch: {} vs {}", + a1.len(), + a2.len() + )); + } + let mut subst = unify(c1, c2)?; + for (a, b) in a1.iter().zip(a2.iter()) { + let s = unify(&a.apply(&subst), &b.apply(&subst))?; + subst = subst.compose(&s); + } + Ok(subst) + } + + // Tuples + (Type::Tuple(a), Type::Tuple(b)) => { + if a.len() != b.len() { + return Err(format!("Tuple size mismatch: {} vs {}", a.len(), b.len())); + } + let mut subst = Substitution::new(); + for (x, y) in a.iter().zip(b.iter()) { + let s = unify(&x.apply(&subst), &y.apply(&subst))?; + subst = subst.compose(&s); + } + Ok(subst) + } + + // Records (structural) + (Type::Record(a), Type::Record(b)) => { + if a.len() != b.len() { + return Err("Record field count mismatch".to_string()); + } + let mut subst = Substitution::new(); + for ((n1, t1), (n2, t2)) in a.iter().zip(b.iter()) { + if n1 != n2 { + return Err(format!("Record field name mismatch: {} vs {}", n1, n2)); + } + let s = unify(&t1.apply(&subst), &t2.apply(&subst))?; + subst = subst.compose(&s); + } + Ok(subst) + } + + // List + (Type::List(a), Type::List(b)) => unify(a, b), + + // Option + (Type::Option(a), Type::Option(b)) => unify(a, b), + + // Versioned types + ( + Type::Versioned { + base: b1, + version: v1, + }, + Type::Versioned { + base: b2, + version: v2, + }, + ) => { + // Check version compatibility + if !versions_compatible(v1, v2) { + return Err(format!( + "Version mismatch: {} is not compatible with {}", + v1, v2 + )); + } + unify(b1, b2) + } + + // Versioned type with non-versioned: treat as the base type + (Type::Versioned { base, .. }, other) | (other, Type::Versioned { base, .. }) => { + unify(base, other) + } + + _ => Err(format!("Cannot unify {} with {}", t1, t2)), + } +} + +/// Check if two version constraints are compatible +fn versions_compatible(v1: &VersionInfo, v2: &VersionInfo) -> bool { + match (v1, v2) { + // Same exact version + (VersionInfo::Exact(a), VersionInfo::Exact(b)) => a == b, + + // AtLeast is compatible if the other version is >= the minimum + (VersionInfo::AtLeast(min), VersionInfo::Exact(v)) + | (VersionInfo::Exact(v), VersionInfo::AtLeast(min)) => v >= min, + + // Two AtLeast constraints: use the higher minimum + (VersionInfo::AtLeast(_), VersionInfo::AtLeast(_)) => true, // compatible, use max + + // Latest is compatible with anything + (VersionInfo::Latest, _) | (_, VersionInfo::Latest) => true, + } +}