init lux
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/result
|
||||||
670
Cargo.lock
generated
Normal file
670
Cargo.lock
generated
Normal file
@@ -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"
|
||||||
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -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
|
||||||
213
README.md
Normal file
213
README.md
Normal file
@@ -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<Int> -- optional field: auto-compatible
|
||||||
|
}
|
||||||
|
|
||||||
|
type User @v3 {
|
||||||
|
fullName: String, -- renamed: requires migration
|
||||||
|
email: String,
|
||||||
|
age: Option<Int>,
|
||||||
|
|
||||||
|
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<F, T>(action: F): Result<T, Error>
|
||||||
|
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<T: Ord>(list: List<T>): List<T>
|
||||||
|
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
|
||||||
734
SKILLS.md
Normal file
734
SKILLS.md
Normal file
@@ -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<T>(q: Query<T>): List<T>
|
||||||
|
fn execute(q: Command): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
effect Logger {
|
||||||
|
fn log(level: Level, message: String): Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
effect Async {
|
||||||
|
fn await<T>(future: Future<T>): T
|
||||||
|
fn spawn<T>(action: fn(): T): Future<T>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Effect Signatures
|
||||||
|
|
||||||
|
Functions declare their effects after `with`:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn fetchUsers(): List<User> 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<String, Response>): 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<E, T>(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<T>(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<T>(error: Error): T -- early return / exceptions
|
||||||
|
}
|
||||||
|
|
||||||
|
effect State<S> {
|
||||||
|
fn get(): S
|
||||||
|
fn put(s: S): Unit
|
||||||
|
fn modify(f: fn(S): S): Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
effect Reader<R> {
|
||||||
|
fn ask(): R
|
||||||
|
}
|
||||||
|
|
||||||
|
effect Random {
|
||||||
|
fn int(range: Range<Int>): Int
|
||||||
|
fn float(): Float
|
||||||
|
fn shuffle<T>(list: List<T>): List<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<User>(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<Int>
|
||||||
|
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<T> = List<T> where self.len > 0
|
||||||
|
type Email = String where self.matches(emailRegex)
|
||||||
|
|
||||||
|
fn head<T>(list: NonEmptyList<T>): 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<T: Ord>(list: List<T>): List<T>
|
||||||
|
is pure,
|
||||||
|
is total,
|
||||||
|
where result.len == list.len,
|
||||||
|
where result.isSorted,
|
||||||
|
where result.isPermutationOf(list)
|
||||||
|
|
||||||
|
fn filter<T>(list: List<T>, pred: fn(T): Bool): List<T>
|
||||||
|
is pure,
|
||||||
|
is total,
|
||||||
|
where result.len <= list.len,
|
||||||
|
where result.all(pred)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Property Requirements
|
||||||
|
|
||||||
|
```lux
|
||||||
|
-- Require properties from function arguments
|
||||||
|
fn retry<F, T>(action: F, times: Int): Result<T, Error>
|
||||||
|
where F: fn(): T with {Fail},
|
||||||
|
where F is idempotent -- enforced at call site!
|
||||||
|
= ...
|
||||||
|
|
||||||
|
fn memoize<F, A, B>(f: F): fn(A): B with {Cache}
|
||||||
|
where F: fn(A): B,
|
||||||
|
where F is pure,
|
||||||
|
where F is deterministic
|
||||||
|
= ...
|
||||||
|
|
||||||
|
fn parallelize<F, T>(actions: List<F>): List<T> 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<T: Ord>(list: List<T>): List<T>
|
||||||
|
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<T: Ord>(list: List<T>): List<T>
|
||||||
|
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<T>
|
||||||
|
Set<T>
|
||||||
|
Map<K, V>
|
||||||
|
Array<T> -- fixed size
|
||||||
|
|
||||||
|
-- Optionality
|
||||||
|
Option<T> = None | Some(T)
|
||||||
|
Result<T, E> = 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<T> =
|
||||||
|
| Leaf(T)
|
||||||
|
| Node(Tree<T>, Tree<T>)
|
||||||
|
|
||||||
|
type Result<T, E> =
|
||||||
|
| 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>): 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 `<E1, E2>`?
|
||||||
|
- [ ] 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)
|
||||||
301
docs/OVERVIEW.md
Normal file
301
docs/OVERVIEW.md
Normal file
@@ -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<Int> => 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
|
||||||
253
docs/VISION.md
Normal file
253
docs/VISION.md
Normal file
@@ -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<F>(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.
|
||||||
46
examples/datatypes.lux
Normal file
46
examples/datatypes.lux
Normal file
@@ -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<Int> =
|
||||||
|
if b == 0 then None
|
||||||
|
else Some(a / b)
|
||||||
|
|
||||||
|
fn showResult(result: Option<Int>): String =
|
||||||
|
match result {
|
||||||
|
None => "Division by zero!",
|
||||||
|
Some(n) => "Result: " + n
|
||||||
|
}
|
||||||
35
examples/effects.lux
Normal file
35
examples/effects.lux
Normal file
@@ -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)
|
||||||
|
}
|
||||||
12
examples/factorial.lux
Normal file
12
examples/factorial.lux
Normal file
@@ -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)
|
||||||
42
examples/functional.lux
Normal file
42
examples/functional.lux
Normal file
@@ -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
|
||||||
5
examples/hello.lux
Normal file
5
examples/hello.lux
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Hello World in Lux
|
||||||
|
// Demonstrates basic effect usage
|
||||||
|
|
||||||
|
fn main(): Unit with {Console} =
|
||||||
|
Console.print("Hello, World!")
|
||||||
96
flake.lock
generated
Normal file
96
flake.lock
generated
Normal file
@@ -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
|
||||||
|
}
|
||||||
55
flake.nix
Normal file
55
flake.nix
Normal file
@@ -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<file.lux>\033[0m Run a file\n"
|
||||||
|
printf "\n"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "lux";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = ./.;
|
||||||
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
583
src/ast.rs
Normal file
583
src/ast.rs
Normal file
@@ -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<String>, 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<std::cmp::Ordering> {
|
||||||
|
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<Ident>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModulePath {
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
self.segments
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.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<Ident>,
|
||||||
|
/// Specific items to import: import foo.{a, b, c}
|
||||||
|
pub items: Option<Vec<Ident>>,
|
||||||
|
/// 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<ImportDecl>,
|
||||||
|
/// Top-level declarations
|
||||||
|
pub declarations: Vec<Declaration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Ident>,
|
||||||
|
pub params: Vec<Parameter>,
|
||||||
|
pub return_type: TypeExpr,
|
||||||
|
pub effects: Vec<Ident>,
|
||||||
|
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<Ident>,
|
||||||
|
pub operations: Vec<EffectOp>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An operation within an effect
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EffectOp {
|
||||||
|
pub name: Ident,
|
||||||
|
pub params: Vec<Parameter>,
|
||||||
|
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<Ident>,
|
||||||
|
/// Optional version annotation: type User @v2 { ... }
|
||||||
|
pub version: Option<Version>,
|
||||||
|
pub definition: TypeDef,
|
||||||
|
/// Migrations from previous versions: from @v1 = { ... }
|
||||||
|
pub migrations: Vec<Migration>,
|
||||||
|
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<RecordField>),
|
||||||
|
/// Enum/ADT: type Foo = A | B(Int) | C { x: Int }
|
||||||
|
Enum(Vec<Variant>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<TypeExpr>),
|
||||||
|
/// Record variant: A { x: Int, y: String }
|
||||||
|
Record(Vec<RecordField>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler declaration
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HandlerDecl {
|
||||||
|
pub name: Ident,
|
||||||
|
pub params: Vec<Parameter>,
|
||||||
|
pub effect: Ident,
|
||||||
|
pub implementations: Vec<HandlerImpl>,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of an effect operation in a handler
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HandlerImpl {
|
||||||
|
pub op_name: Ident,
|
||||||
|
pub params: Vec<Ident>,
|
||||||
|
pub resume: Option<Ident>, // 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<TypeExpr>,
|
||||||
|
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<Int>, Map<String, Int>
|
||||||
|
App(Box<TypeExpr>, Vec<TypeExpr>),
|
||||||
|
/// Function type: fn(A, B): C
|
||||||
|
Function {
|
||||||
|
params: Vec<TypeExpr>,
|
||||||
|
return_type: Box<TypeExpr>,
|
||||||
|
effects: Vec<Ident>,
|
||||||
|
},
|
||||||
|
/// Tuple type: (A, B, C)
|
||||||
|
Tuple(Vec<TypeExpr>),
|
||||||
|
/// Record type: { name: String, age: Int }
|
||||||
|
Record(Vec<RecordField>),
|
||||||
|
/// Unit type
|
||||||
|
Unit,
|
||||||
|
/// Versioned type: User @v2, User @v2+, User @latest
|
||||||
|
Versioned {
|
||||||
|
base: Box<TypeExpr>,
|
||||||
|
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<Expr>,
|
||||||
|
right: Box<Expr>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Unary operation: -a, !a
|
||||||
|
UnaryOp {
|
||||||
|
op: UnaryOp,
|
||||||
|
operand: Box<Expr>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Function call: foo(a, b)
|
||||||
|
Call {
|
||||||
|
func: Box<Expr>,
|
||||||
|
args: Vec<Expr>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Effect operation call: Effect.operation(args)
|
||||||
|
EffectOp {
|
||||||
|
effect: Ident,
|
||||||
|
operation: Ident,
|
||||||
|
args: Vec<Expr>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Field access: foo.bar
|
||||||
|
Field {
|
||||||
|
object: Box<Expr>,
|
||||||
|
field: Ident,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1
|
||||||
|
Lambda {
|
||||||
|
params: Vec<Parameter>,
|
||||||
|
return_type: Option<Box<TypeExpr>>,
|
||||||
|
effects: Vec<Ident>,
|
||||||
|
body: Box<Expr>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Let binding: let x = e1; e2
|
||||||
|
Let {
|
||||||
|
name: Ident,
|
||||||
|
typ: Option<TypeExpr>,
|
||||||
|
value: Box<Expr>,
|
||||||
|
body: Box<Expr>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// If expression: if cond then e1 else e2
|
||||||
|
If {
|
||||||
|
condition: Box<Expr>,
|
||||||
|
then_branch: Box<Expr>,
|
||||||
|
else_branch: Box<Expr>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Match expression
|
||||||
|
Match {
|
||||||
|
scrutinee: Box<Expr>,
|
||||||
|
arms: Vec<MatchArm>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Block: { e1; e2; e3 }
|
||||||
|
Block {
|
||||||
|
statements: Vec<Statement>,
|
||||||
|
result: Box<Expr>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Record literal: { name: "Alice", age: 30 }
|
||||||
|
Record {
|
||||||
|
fields: Vec<(Ident, Expr)>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Tuple literal: (1, "hello", true)
|
||||||
|
Tuple { elements: Vec<Expr>, span: Span },
|
||||||
|
/// List literal: [1, 2, 3]
|
||||||
|
List { elements: Vec<Expr>, span: Span },
|
||||||
|
/// Run with handlers: run expr with { Effect = handler, ... }
|
||||||
|
Run {
|
||||||
|
expr: Box<Expr>,
|
||||||
|
handlers: Vec<(Ident, Expr)>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Resume continuation in handler (like calling the continuation)
|
||||||
|
Resume { value: Box<Expr>, 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<TypeExpr>,
|
||||||
|
value: Expr,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match arm
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MatchArm {
|
||||||
|
pub pattern: Pattern,
|
||||||
|
pub guard: Option<Expr>,
|
||||||
|
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<Pattern>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Record pattern: { name, age: a }
|
||||||
|
Record {
|
||||||
|
fields: Vec<(Ident, Pattern)>,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
|
/// Tuple pattern: (a, b, c)
|
||||||
|
Tuple { elements: Vec<Pattern>, 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2202
src/interpreter.rs
Normal file
2202
src/interpreter.rs
Normal file
File diff suppressed because it is too large
Load Diff
633
src/lexer.rs
Normal file
633
src/lexer.rs
Normal file
@@ -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<Chars<'a>>,
|
||||||
|
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<Vec<Token>, 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<Token, LexError> {
|
||||||
|
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<char> {
|
||||||
|
let c = self.chars.next()?;
|
||||||
|
self.pos += c.len_utf8();
|
||||||
|
Some(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek(&mut self) -> Option<char> {
|
||||||
|
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<TokenKind, LexError> {
|
||||||
|
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<TokenKind, LexError> {
|
||||||
|
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<TokenKind, LexError> {
|
||||||
|
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<TokenKind> {
|
||||||
|
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
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
791
src/main.rs
Normal file
791
src/main.rs
Normal file
@@ -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 <expr> Show the type of an expression
|
||||||
|
:clear Clear the environment
|
||||||
|
:load <file> 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<String> = 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 <expression>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
":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 <filename>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
":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<String, String> {
|
||||||
|
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::<Vec<_>>()
|
||||||
|
.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<Int> => 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<Int> => 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
634
src/modules.rs
Normal file
634
src/modules.rs
Normal file
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<PathBuf>,
|
||||||
|
/// Cache of loaded modules (path -> module)
|
||||||
|
cache: HashMap<String, Module>,
|
||||||
|
/// Modules currently being loaded (for circular dependency detection)
|
||||||
|
loading: HashSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<PathBuf>) -> 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<PathBuf> {
|
||||||
|
// 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<Module, ModuleError> {
|
||||||
|
// 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<Program, ModuleError> {
|
||||||
|
// 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: "<main>".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<String> {
|
||||||
|
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<Item = (&String, &Module)> {
|
||||||
|
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<HashMap<String, ResolvedImport>, 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>): 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
1935
src/parser.rs
Normal file
1935
src/parser.rs
Normal file
File diff suppressed because it is too large
Load Diff
330
src/schema.rs
Normal file
330
src/schema.rs
Normal file
@@ -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<AutoMigration>),
|
||||||
|
/// Breaking change - requires explicit migration
|
||||||
|
Breaking(Vec<BreakingChange>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String, HashMap<u32, VersionedTypeDef>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A versioned type definition with its migrations
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VersionedTypeDef {
|
||||||
|
pub version: u32,
|
||||||
|
pub definition: TypeDef,
|
||||||
|
pub migrations: Vec<Migration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<u32, VersionedTypeDef>> {
|
||||||
|
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<u32> {
|
||||||
|
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<Compatibility, String> {
|
||||||
|
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<Vec<(u32, u32)>, 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<Compatibility, String> {
|
||||||
|
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: "<alias>".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<Compatibility, String> {
|
||||||
|
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(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
1228
src/typechecker.rs
Normal file
1228
src/typechecker.rs
Normal file
File diff suppressed because it is too large
Load Diff
1083
src/types.rs
Normal file
1083
src/types.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user