chore(users): grfn -> aspen
Change-Id: I6c6847fac56f0a9a1a2209792e00a3aec5e672b9 Reviewed-on: https://cl.tvl.fyi/c/depot/+/10809 Autosubmit: aspen <root@gws.fyi> Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI Reviewed-by: lukegb <lukegb@tvl.fyi>
This commit is contained in:
parent
0ba476a426
commit
82ecd61f5c
478 changed files with 75 additions and 77 deletions
3
users/aspen/OWNERS
Normal file
3
users/aspen/OWNERS
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
set noparent
|
||||
|
||||
aspen
|
||||
2
users/aspen/achilles/.envrc
Normal file
2
users/aspen/achilles/.envrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
source_up
|
||||
eval "$(lorri direnv)"
|
||||
1
users/aspen/achilles/.gitignore
vendored
Normal file
1
users/aspen/achilles/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
885
users/aspen/achilles/Cargo.lock
generated
Normal file
885
users/aspen/achilles/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,885 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "achilles"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bimap",
|
||||
"clap",
|
||||
"crate-root",
|
||||
"derive_more",
|
||||
"inkwell",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"llvm-sys",
|
||||
"nom",
|
||||
"nom-trace",
|
||||
"pratt",
|
||||
"pretty_assertions",
|
||||
"proptest",
|
||||
"test-strategy",
|
||||
"thiserror",
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bimap"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0455254eb5c6964c4545d8bac815e1a1be4f3afe0ae695ea539c12d728d44b"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "0.19.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "crate-root"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59c6fe4622b269032d2c5140a592d67a9c409031d286174fcde172fbed86f0d3"
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inkwell"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/TheDan64/inkwell?branch=master#6ab2b19e1b90be55fa4f9f056f29bd1ed557b990"
|
||||
dependencies = [
|
||||
"either",
|
||||
"inkwell_internals",
|
||||
"libc",
|
||||
"llvm-sys",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inkwell_internals"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/TheDan64/inkwell?branch=master#6ab2b19e1b90be55fa4f9f056f29bd1ed557b990"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lexical-core"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
||||
|
||||
[[package]]
|
||||
name = "llvm-sys"
|
||||
version = "110.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b918288a585ac36703abefcbc5d4c43137b604ec0c2d39abefb55e25c7501dc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"regex",
|
||||
"semver 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "6.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"funty",
|
||||
"lexical-core",
|
||||
"memchr",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom-trace"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/glittershark/nom-trace?branch=nom-6#6168d2e15cc51efd12d80260159b76a764dba138"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||
|
||||
[[package]]
|
||||
name = "output_vt100"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
|
||||
dependencies = [
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||
|
||||
[[package]]
|
||||
name = "pratt"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bbc12f7936a7b195790dd6d9b982b66c54f45ff6766decf25c44cac302dce"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"ctor",
|
||||
"diff",
|
||||
"output_vt100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proptest"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"quick-error 2.0.1",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver 1.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error 1.2.3",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||
dependencies = [
|
||||
"semver-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd"
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "structmeta"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd9c2155aa89fb2c2cb87d99a610c689e7c47099b3e9f1c8a8f53faf4e3d2e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structmeta-derive"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bafede0d0a2f21910f36d47b1558caae3076ed80f6f3ad0fc85a91e6ba7e5938"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-strategy"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22c726321a7c108ca1de4ed2e6a362ead7193ecfbe0b326c5dff602b65a09e6a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
|
||||
26
users/aspen/achilles/Cargo.toml
Normal file
26
users/aspen/achilles/Cargo.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "achilles"
|
||||
version = "0.1.0"
|
||||
authors = ["Griffin Smith <root@gws.fyi>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
bimap = "0.6.0"
|
||||
clap = "3.0.0-beta.2"
|
||||
derive_more = "0.99.11"
|
||||
inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", features = ["llvm11-0"] }
|
||||
itertools = "0.10.0"
|
||||
lazy_static = "1.4.0"
|
||||
llvm-sys = "110.0.1"
|
||||
nom = "6.1.2"
|
||||
nom-trace = { git = "https://github.com/glittershark/nom-trace", branch = "nom-6" }
|
||||
pratt = "0.3.0"
|
||||
proptest = "1.0.0"
|
||||
test-strategy = "0.1.1"
|
||||
thiserror = "1.0.24"
|
||||
void = "1.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
crate-root = "0.1.3"
|
||||
pretty_assertions = "0.7.1"
|
||||
7
users/aspen/achilles/ach/.gitignore
vendored
Normal file
7
users/aspen/achilles/ach/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
*.ll
|
||||
*.o
|
||||
|
||||
functions
|
||||
simple
|
||||
externs
|
||||
units
|
||||
15
users/aspen/achilles/ach/Makefile
Normal file
15
users/aspen/achilles/ach/Makefile
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
default: simple
|
||||
|
||||
%.ll: %.ach
|
||||
cargo run -- compile $< -o $@ -f llvm
|
||||
|
||||
%.o: %.ll
|
||||
llc $< -o $@ -filetype=obj
|
||||
|
||||
%: %.o
|
||||
clang $< -o $@
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
clean:
|
||||
@rm -f *.ll *.o simple functions
|
||||
5
users/aspen/achilles/ach/externs.ach
Normal file
5
users/aspen/achilles/ach/externs.ach
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
extern puts : fn cstring -> int
|
||||
|
||||
fn main =
|
||||
let _ = puts "foobar"
|
||||
in 0
|
||||
8
users/aspen/achilles/ach/functions.ach
Normal file
8
users/aspen/achilles/ach/functions.ach
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
ty id : fn a -> a
|
||||
fn id x = x
|
||||
|
||||
ty plus : fn int -> int
|
||||
fn plus (x: int) (y: int) = x + y
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = plus (id 2) 7
|
||||
1
users/aspen/achilles/ach/simple.ach
Normal file
1
users/aspen/achilles/ach/simple.ach
Normal file
|
|
@ -0,0 +1 @@
|
|||
fn main = let x = 2; y = 3 in x + y
|
||||
7
users/aspen/achilles/ach/units.ach
Normal file
7
users/aspen/achilles/ach/units.ach
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extern puts : fn cstring -> int
|
||||
|
||||
ty print : fn cstring -> ()
|
||||
fn print x = let _ = puts x in ()
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = let _ = print "hi" in 0
|
||||
27
users/aspen/achilles/default.nix
Normal file
27
users/aspen/achilles/default.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{ depot, pkgs, ... }:
|
||||
|
||||
let
|
||||
llvmPackages = pkgs.llvmPackages_11;
|
||||
in
|
||||
|
||||
depot.third_party.naersk.buildPackage {
|
||||
src = ./.;
|
||||
|
||||
buildInputs = [
|
||||
llvmPackages.clang
|
||||
llvmPackages.llvm
|
||||
llvmPackages.bintools
|
||||
llvmPackages.libclang.lib
|
||||
] ++ (with pkgs; [
|
||||
zlib
|
||||
ncurses
|
||||
libxml2
|
||||
libffi
|
||||
pkg-config
|
||||
]);
|
||||
|
||||
doCheck = true;
|
||||
|
||||
# Trouble linking against LLVM, maybe since rustc's llvmPackages got bumped?
|
||||
meta.ci.skip = true;
|
||||
}
|
||||
18
users/aspen/achilles/shell.nix
Normal file
18
users/aspen/achilles/shell.nix
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
with (import ../../.. { }).third_party.nixpkgs;
|
||||
|
||||
mkShell {
|
||||
buildInputs = [
|
||||
clang_11
|
||||
llvm_11.lib
|
||||
llvmPackages_11.bintools
|
||||
llvmPackages_11.clang
|
||||
llvmPackages_11.libclang.lib
|
||||
zlib
|
||||
ncurses
|
||||
libxml2
|
||||
libffi
|
||||
pkg-config
|
||||
];
|
||||
|
||||
LLVM_SYS_110_PREFIX = llvmPackages_11.bintools;
|
||||
}
|
||||
364
users/aspen/achilles/src/ast/hir.rs
Normal file
364
users/aspen/achilles/src/ast/hir.rs
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{BinaryOperator, Ident, Literal, UnaryOperator};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Pattern<'a, T> {
|
||||
Id(Ident<'a>, T),
|
||||
Tuple(Vec<Pattern<'a, T>>),
|
||||
}
|
||||
|
||||
impl<'a, T> Pattern<'a, T> {
|
||||
pub fn to_owned(&self) -> Pattern<'static, T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
match self {
|
||||
Pattern::Id(id, t) => Pattern::Id(id.to_owned(), t.clone()),
|
||||
Pattern::Tuple(pats) => {
|
||||
Pattern::Tuple(pats.into_iter().map(Pattern::to_owned).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn traverse_type<F, U, E>(self, f: F) -> Result<Pattern<'a, U>, E>
|
||||
where
|
||||
F: Fn(T) -> Result<U, E> + Clone,
|
||||
{
|
||||
match self {
|
||||
Pattern::Id(id, t) => Ok(Pattern::Id(id, f(t)?)),
|
||||
Pattern::Tuple(pats) => Ok(Pattern::Tuple(
|
||||
pats.into_iter()
|
||||
.map(|pat| pat.traverse_type(f.clone()))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Binding<'a, T> {
|
||||
pub pat: Pattern<'a, T>,
|
||||
pub body: Expr<'a, T>,
|
||||
}
|
||||
|
||||
impl<'a, T> Binding<'a, T> {
|
||||
fn to_owned(&self) -> Binding<'static, T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
Binding {
|
||||
pat: self.pat.to_owned(),
|
||||
body: self.body.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Expr<'a, T> {
|
||||
Ident(Ident<'a>, T),
|
||||
|
||||
Literal(Literal<'a>, T),
|
||||
|
||||
Tuple(Vec<Expr<'a, T>>, T),
|
||||
|
||||
UnaryOp {
|
||||
op: UnaryOperator,
|
||||
rhs: Box<Expr<'a, T>>,
|
||||
type_: T,
|
||||
},
|
||||
|
||||
BinaryOp {
|
||||
lhs: Box<Expr<'a, T>>,
|
||||
op: BinaryOperator,
|
||||
rhs: Box<Expr<'a, T>>,
|
||||
type_: T,
|
||||
},
|
||||
|
||||
Let {
|
||||
bindings: Vec<Binding<'a, T>>,
|
||||
body: Box<Expr<'a, T>>,
|
||||
type_: T,
|
||||
},
|
||||
|
||||
If {
|
||||
condition: Box<Expr<'a, T>>,
|
||||
then: Box<Expr<'a, T>>,
|
||||
else_: Box<Expr<'a, T>>,
|
||||
type_: T,
|
||||
},
|
||||
|
||||
Fun {
|
||||
type_args: Vec<Ident<'a>>,
|
||||
args: Vec<(Ident<'a>, T)>,
|
||||
body: Box<Expr<'a, T>>,
|
||||
type_: T,
|
||||
},
|
||||
|
||||
Call {
|
||||
fun: Box<Expr<'a, T>>,
|
||||
type_args: HashMap<Ident<'a>, T>,
|
||||
args: Vec<Expr<'a, T>>,
|
||||
type_: T,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a, T> Expr<'a, T> {
|
||||
pub fn type_(&self) -> &T {
|
||||
match self {
|
||||
Expr::Ident(_, t) => t,
|
||||
Expr::Literal(_, t) => t,
|
||||
Expr::Tuple(_, t) => t,
|
||||
Expr::UnaryOp { type_, .. } => type_,
|
||||
Expr::BinaryOp { type_, .. } => type_,
|
||||
Expr::Let { type_, .. } => type_,
|
||||
Expr::If { type_, .. } => type_,
|
||||
Expr::Fun { type_, .. } => type_,
|
||||
Expr::Call { type_, .. } => type_,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn traverse_type<F, U, E>(self, f: F) -> Result<Expr<'a, U>, E>
|
||||
where
|
||||
F: Fn(T) -> Result<U, E> + Clone,
|
||||
{
|
||||
match self {
|
||||
Expr::Ident(id, t) => Ok(Expr::Ident(id, f(t)?)),
|
||||
Expr::Literal(lit, t) => Ok(Expr::Literal(lit, f(t)?)),
|
||||
Expr::UnaryOp { op, rhs, type_ } => Ok(Expr::UnaryOp {
|
||||
op,
|
||||
rhs: Box::new(rhs.traverse_type(f.clone())?),
|
||||
type_: f(type_)?,
|
||||
}),
|
||||
Expr::BinaryOp {
|
||||
lhs,
|
||||
op,
|
||||
rhs,
|
||||
type_,
|
||||
} => Ok(Expr::BinaryOp {
|
||||
lhs: Box::new(lhs.traverse_type(f.clone())?),
|
||||
op,
|
||||
rhs: Box::new(rhs.traverse_type(f.clone())?),
|
||||
type_: f(type_)?,
|
||||
}),
|
||||
Expr::Let {
|
||||
bindings,
|
||||
body,
|
||||
type_,
|
||||
} => Ok(Expr::Let {
|
||||
bindings: bindings
|
||||
.into_iter()
|
||||
.map(|Binding { pat, body }| {
|
||||
Ok(Binding {
|
||||
pat: pat.traverse_type(f.clone())?,
|
||||
body: body.traverse_type(f.clone())?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, E>>()?,
|
||||
body: Box::new(body.traverse_type(f.clone())?),
|
||||
type_: f(type_)?,
|
||||
}),
|
||||
Expr::If {
|
||||
condition,
|
||||
then,
|
||||
else_,
|
||||
type_,
|
||||
} => Ok(Expr::If {
|
||||
condition: Box::new(condition.traverse_type(f.clone())?),
|
||||
then: Box::new(then.traverse_type(f.clone())?),
|
||||
else_: Box::new(else_.traverse_type(f.clone())?),
|
||||
type_: f(type_)?,
|
||||
}),
|
||||
Expr::Fun {
|
||||
args,
|
||||
type_args,
|
||||
body,
|
||||
type_,
|
||||
} => Ok(Expr::Fun {
|
||||
args: args
|
||||
.into_iter()
|
||||
.map(|(id, t)| Ok((id, f.clone()(t)?)))
|
||||
.collect::<Result<Vec<_>, E>>()?,
|
||||
type_args,
|
||||
body: Box::new(body.traverse_type(f.clone())?),
|
||||
type_: f(type_)?,
|
||||
}),
|
||||
Expr::Call {
|
||||
fun,
|
||||
type_args,
|
||||
args,
|
||||
type_,
|
||||
} => Ok(Expr::Call {
|
||||
fun: Box::new(fun.traverse_type(f.clone())?),
|
||||
type_args: type_args
|
||||
.into_iter()
|
||||
.map(|(id, ty)| Ok((id, f.clone()(ty)?)))
|
||||
.collect::<Result<HashMap<_, _>, E>>()?,
|
||||
args: args
|
||||
.into_iter()
|
||||
.map(|e| e.traverse_type(f.clone()))
|
||||
.collect::<Result<Vec<_>, E>>()?,
|
||||
type_: f(type_)?,
|
||||
}),
|
||||
Expr::Tuple(members, t) => Ok(Expr::Tuple(
|
||||
members
|
||||
.into_iter()
|
||||
.map(|t| t.traverse_type(f.clone()))
|
||||
.try_collect()?,
|
||||
f(t)?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_owned(&self) -> Expr<'static, T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
match self {
|
||||
Expr::Ident(id, t) => Expr::Ident(id.to_owned(), t.clone()),
|
||||
Expr::Literal(lit, t) => Expr::Literal(lit.to_owned(), t.clone()),
|
||||
Expr::UnaryOp { op, rhs, type_ } => Expr::UnaryOp {
|
||||
op: *op,
|
||||
rhs: Box::new((**rhs).to_owned()),
|
||||
type_: type_.clone(),
|
||||
},
|
||||
Expr::BinaryOp {
|
||||
lhs,
|
||||
op,
|
||||
rhs,
|
||||
type_,
|
||||
} => Expr::BinaryOp {
|
||||
lhs: Box::new((**lhs).to_owned()),
|
||||
op: *op,
|
||||
rhs: Box::new((**rhs).to_owned()),
|
||||
type_: type_.clone(),
|
||||
},
|
||||
Expr::Let {
|
||||
bindings,
|
||||
body,
|
||||
type_,
|
||||
} => Expr::Let {
|
||||
bindings: bindings.iter().map(|b| b.to_owned()).collect(),
|
||||
body: Box::new((**body).to_owned()),
|
||||
type_: type_.clone(),
|
||||
},
|
||||
Expr::If {
|
||||
condition,
|
||||
then,
|
||||
else_,
|
||||
type_,
|
||||
} => Expr::If {
|
||||
condition: Box::new((**condition).to_owned()),
|
||||
then: Box::new((**then).to_owned()),
|
||||
else_: Box::new((**else_).to_owned()),
|
||||
type_: type_.clone(),
|
||||
},
|
||||
Expr::Fun {
|
||||
args,
|
||||
type_args,
|
||||
body,
|
||||
type_,
|
||||
} => Expr::Fun {
|
||||
args: args
|
||||
.iter()
|
||||
.map(|(id, t)| (id.to_owned(), t.clone()))
|
||||
.collect(),
|
||||
type_args: type_args.iter().map(|arg| arg.to_owned()).collect(),
|
||||
body: Box::new((**body).to_owned()),
|
||||
type_: type_.clone(),
|
||||
},
|
||||
Expr::Call {
|
||||
fun,
|
||||
type_args,
|
||||
args,
|
||||
type_,
|
||||
} => Expr::Call {
|
||||
fun: Box::new((**fun).to_owned()),
|
||||
type_args: type_args
|
||||
.iter()
|
||||
.map(|(id, t)| (id.to_owned(), t.clone()))
|
||||
.collect(),
|
||||
args: args.iter().map(|e| e.to_owned()).collect(),
|
||||
type_: type_.clone(),
|
||||
},
|
||||
Expr::Tuple(members, t) => {
|
||||
Expr::Tuple(members.into_iter().map(Expr::to_owned).collect(), t.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Decl<'a, T> {
|
||||
Fun {
|
||||
name: Ident<'a>,
|
||||
type_args: Vec<Ident<'a>>,
|
||||
args: Vec<(Ident<'a>, T)>,
|
||||
body: Box<Expr<'a, T>>,
|
||||
type_: T,
|
||||
},
|
||||
|
||||
Extern {
|
||||
name: Ident<'a>,
|
||||
arg_types: Vec<T>,
|
||||
ret_type: T,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a, T> Decl<'a, T> {
|
||||
pub fn name(&self) -> &Ident<'a> {
|
||||
match self {
|
||||
Decl::Fun { name, .. } => name,
|
||||
Decl::Extern { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, new_name: Ident<'a>) {
|
||||
match self {
|
||||
Decl::Fun { name, .. } => *name = new_name,
|
||||
Decl::Extern { name, .. } => *name = new_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_(&self) -> Option<&T> {
|
||||
match self {
|
||||
Decl::Fun { type_, .. } => Some(type_),
|
||||
Decl::Extern { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn traverse_type<F, U, E>(self, f: F) -> Result<Decl<'a, U>, E>
|
||||
where
|
||||
F: Fn(T) -> Result<U, E> + Clone,
|
||||
{
|
||||
match self {
|
||||
Decl::Fun {
|
||||
name,
|
||||
type_args,
|
||||
args,
|
||||
body,
|
||||
type_,
|
||||
} => Ok(Decl::Fun {
|
||||
name,
|
||||
type_args,
|
||||
args: args
|
||||
.into_iter()
|
||||
.map(|(id, t)| Ok((id, f(t)?)))
|
||||
.try_collect()?,
|
||||
body: Box::new(body.traverse_type(f.clone())?),
|
||||
type_: f(type_)?,
|
||||
}),
|
||||
Decl::Extern {
|
||||
name,
|
||||
arg_types,
|
||||
ret_type,
|
||||
} => Ok(Decl::Extern {
|
||||
name,
|
||||
arg_types: arg_types.into_iter().map(f.clone()).try_collect()?,
|
||||
ret_type: f(ret_type)?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
484
users/aspen/achilles/src/ast/mod.rs
Normal file
484
users/aspen/achilles/src/ast/mod.rs
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
pub(crate) mod hir;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct InvalidIdentifier<'a>(Cow<'a, str>);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct Ident<'a>(pub Cow<'a, str>);
|
||||
|
||||
impl<'a> From<&'a Ident<'a>> for &'a str {
|
||||
fn from(id: &'a Ident<'a>) -> Self {
|
||||
id.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Ident<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Ident<'a> {
|
||||
pub fn to_owned(&self) -> Ident<'static> {
|
||||
Ident(Cow::Owned(self.0.clone().into_owned()))
|
||||
}
|
||||
|
||||
/// Construct an identifier from a &str without checking that it's a valid identifier
|
||||
pub fn from_str_unchecked(s: &'a str) -> Self {
|
||||
debug_assert!(is_valid_identifier(s));
|
||||
Self(Cow::Borrowed(s))
|
||||
}
|
||||
|
||||
pub fn from_string_unchecked(s: String) -> Self {
|
||||
debug_assert!(is_valid_identifier(&s));
|
||||
Self(Cow::Owned(s))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid_identifier<S>(s: &S) -> bool
|
||||
where
|
||||
S: AsRef<str> + ?Sized,
|
||||
{
|
||||
s.as_ref()
|
||||
.chars()
|
||||
.any(|c| !c.is_alphanumeric() || !"_".contains(c))
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for Ident<'a> {
|
||||
type Error = InvalidIdentifier<'a>;
|
||||
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
if is_valid_identifier(s) {
|
||||
Ok(Ident(Cow::Borrowed(s)))
|
||||
} else {
|
||||
Err(InvalidIdentifier(Cow::Borrowed(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<String> for Ident<'a> {
|
||||
type Error = InvalidIdentifier<'static>;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
if is_valid_identifier(&s) {
|
||||
Ok(Ident(Cow::Owned(s)))
|
||||
} else {
|
||||
Err(InvalidIdentifier(Cow::Owned(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum BinaryOperator {
|
||||
/// `+`
|
||||
Add,
|
||||
|
||||
/// `-`
|
||||
Sub,
|
||||
|
||||
/// `*`
|
||||
Mul,
|
||||
|
||||
/// `/`
|
||||
Div,
|
||||
|
||||
/// `^`
|
||||
Pow,
|
||||
|
||||
/// `==`
|
||||
Equ,
|
||||
|
||||
/// `!=`
|
||||
Neq,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum UnaryOperator {
|
||||
/// !
|
||||
Not,
|
||||
|
||||
/// -
|
||||
Neg,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Literal<'a> {
|
||||
Unit,
|
||||
Int(u64),
|
||||
Bool(bool),
|
||||
String(Cow<'a, str>),
|
||||
}
|
||||
|
||||
impl<'a> Literal<'a> {
|
||||
pub fn to_owned(&self) -> Literal<'static> {
|
||||
match self {
|
||||
Literal::Int(i) => Literal::Int(*i),
|
||||
Literal::Bool(b) => Literal::Bool(*b),
|
||||
Literal::String(s) => Literal::String(Cow::Owned(s.clone().into_owned())),
|
||||
Literal::Unit => Literal::Unit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Pattern<'a> {
|
||||
Id(Ident<'a>),
|
||||
Tuple(Vec<Pattern<'a>>),
|
||||
}
|
||||
|
||||
impl<'a> Pattern<'a> {
|
||||
pub fn to_owned(&self) -> Pattern<'static> {
|
||||
match self {
|
||||
Pattern::Id(id) => Pattern::Id(id.to_owned()),
|
||||
Pattern::Tuple(pats) => Pattern::Tuple(pats.iter().map(Pattern::to_owned).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Binding<'a> {
|
||||
pub pat: Pattern<'a>,
|
||||
pub type_: Option<Type<'a>>,
|
||||
pub body: Expr<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Binding<'a> {
|
||||
fn to_owned(&self) -> Binding<'static> {
|
||||
Binding {
|
||||
pat: self.pat.to_owned(),
|
||||
type_: self.type_.as_ref().map(|t| t.to_owned()),
|
||||
body: self.body.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Expr<'a> {
|
||||
Ident(Ident<'a>),
|
||||
|
||||
Literal(Literal<'a>),
|
||||
|
||||
UnaryOp {
|
||||
op: UnaryOperator,
|
||||
rhs: Box<Expr<'a>>,
|
||||
},
|
||||
|
||||
BinaryOp {
|
||||
lhs: Box<Expr<'a>>,
|
||||
op: BinaryOperator,
|
||||
rhs: Box<Expr<'a>>,
|
||||
},
|
||||
|
||||
Let {
|
||||
bindings: Vec<Binding<'a>>,
|
||||
body: Box<Expr<'a>>,
|
||||
},
|
||||
|
||||
If {
|
||||
condition: Box<Expr<'a>>,
|
||||
then: Box<Expr<'a>>,
|
||||
else_: Box<Expr<'a>>,
|
||||
},
|
||||
|
||||
Fun(Box<Fun<'a>>),
|
||||
|
||||
Call {
|
||||
fun: Box<Expr<'a>>,
|
||||
args: Vec<Expr<'a>>,
|
||||
},
|
||||
|
||||
Tuple(Vec<Expr<'a>>),
|
||||
|
||||
Ascription {
|
||||
expr: Box<Expr<'a>>,
|
||||
type_: Type<'a>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Expr<'a> {
|
||||
pub fn to_owned(&self) -> Expr<'static> {
|
||||
match self {
|
||||
Expr::Ident(ref id) => Expr::Ident(id.to_owned()),
|
||||
Expr::Literal(ref lit) => Expr::Literal(lit.to_owned()),
|
||||
Expr::Tuple(ref members) => {
|
||||
Expr::Tuple(members.into_iter().map(Expr::to_owned).collect())
|
||||
}
|
||||
Expr::UnaryOp { op, rhs } => Expr::UnaryOp {
|
||||
op: *op,
|
||||
rhs: Box::new((**rhs).to_owned()),
|
||||
},
|
||||
Expr::BinaryOp { lhs, op, rhs } => Expr::BinaryOp {
|
||||
lhs: Box::new((**lhs).to_owned()),
|
||||
op: *op,
|
||||
rhs: Box::new((**rhs).to_owned()),
|
||||
},
|
||||
Expr::Let { bindings, body } => Expr::Let {
|
||||
bindings: bindings.iter().map(|binding| binding.to_owned()).collect(),
|
||||
body: Box::new((**body).to_owned()),
|
||||
},
|
||||
Expr::If {
|
||||
condition,
|
||||
then,
|
||||
else_,
|
||||
} => Expr::If {
|
||||
condition: Box::new((**condition).to_owned()),
|
||||
then: Box::new((**then).to_owned()),
|
||||
else_: Box::new((**else_).to_owned()),
|
||||
},
|
||||
Expr::Fun(fun) => Expr::Fun(Box::new((**fun).to_owned())),
|
||||
Expr::Call { fun, args } => Expr::Call {
|
||||
fun: Box::new((**fun).to_owned()),
|
||||
args: args.iter().map(|arg| arg.to_owned()).collect(),
|
||||
},
|
||||
Expr::Ascription { expr, type_ } => Expr::Ascription {
|
||||
expr: Box::new((**expr).to_owned()),
|
||||
type_: type_.to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Arg<'a> {
|
||||
pub ident: Ident<'a>,
|
||||
pub type_: Option<Type<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Arg<'a> {
|
||||
pub fn to_owned(&self) -> Arg<'static> {
|
||||
Arg {
|
||||
ident: self.ident.to_owned(),
|
||||
type_: self.type_.as_ref().map(Type::to_owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for Arg<'a> {
|
||||
type Error = <Ident<'a> as TryFrom<&'a str>>::Error;
|
||||
|
||||
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
|
||||
Ok(Arg {
|
||||
ident: Ident::try_from(value)?,
|
||||
type_: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Fun<'a> {
|
||||
pub args: Vec<Arg<'a>>,
|
||||
pub body: Expr<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Fun<'a> {
|
||||
pub fn to_owned(&self) -> Fun<'static> {
|
||||
Fun {
|
||||
args: self.args.iter().map(|arg| arg.to_owned()).collect(),
|
||||
body: self.body.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Decl<'a> {
|
||||
Fun {
|
||||
name: Ident<'a>,
|
||||
body: Fun<'a>,
|
||||
},
|
||||
Ascription {
|
||||
name: Ident<'a>,
|
||||
type_: Type<'a>,
|
||||
},
|
||||
Extern {
|
||||
name: Ident<'a>,
|
||||
type_: FunctionType<'a>,
|
||||
},
|
||||
}
|
||||
|
||||
////
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FunctionType<'a> {
|
||||
pub args: Vec<Type<'a>>,
|
||||
pub ret: Box<Type<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> FunctionType<'a> {
|
||||
pub fn to_owned(&self) -> FunctionType<'static> {
|
||||
FunctionType {
|
||||
args: self.args.iter().map(|a| a.to_owned()).collect(),
|
||||
ret: Box::new((*self.ret).to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for FunctionType<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "fn {} -> {}", self.args.iter().join(", "), self.ret)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Type<'a> {
|
||||
Int,
|
||||
Float,
|
||||
Bool,
|
||||
CString,
|
||||
Unit,
|
||||
Tuple(Vec<Type<'a>>),
|
||||
Var(Ident<'a>),
|
||||
Function(FunctionType<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Type<'a> {
|
||||
pub fn to_owned(&self) -> Type<'static> {
|
||||
match self {
|
||||
Type::Int => Type::Int,
|
||||
Type::Float => Type::Float,
|
||||
Type::Bool => Type::Bool,
|
||||
Type::CString => Type::CString,
|
||||
Type::Unit => Type::Unit,
|
||||
Type::Var(v) => Type::Var(v.to_owned()),
|
||||
Type::Function(f) => Type::Function(f.to_owned()),
|
||||
Type::Tuple(members) => Type::Tuple(members.iter().map(Type::to_owned).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alpha_equiv(&self, other: &Self) -> bool {
|
||||
fn do_alpha_equiv<'a>(
|
||||
substs: &mut HashMap<&'a Ident<'a>, &'a Ident<'a>>,
|
||||
lhs: &'a Type,
|
||||
rhs: &'a Type,
|
||||
) -> bool {
|
||||
match (lhs, rhs) {
|
||||
(Type::Var(v1), Type::Var(v2)) => substs.entry(v1).or_insert(v2) == &v2,
|
||||
(
|
||||
Type::Function(FunctionType {
|
||||
args: args1,
|
||||
ret: ret1,
|
||||
}),
|
||||
Type::Function(FunctionType {
|
||||
args: args2,
|
||||
ret: ret2,
|
||||
}),
|
||||
) => {
|
||||
args1.len() == args2.len()
|
||||
&& args1
|
||||
.iter()
|
||||
.zip(args2)
|
||||
.all(|(a1, a2)| do_alpha_equiv(substs, a1, a2))
|
||||
&& do_alpha_equiv(substs, ret1, ret2)
|
||||
}
|
||||
_ => lhs == rhs,
|
||||
}
|
||||
}
|
||||
|
||||
let mut substs = HashMap::new();
|
||||
do_alpha_equiv(&mut substs, self, other)
|
||||
}
|
||||
|
||||
pub fn traverse_type_vars<'b, F>(self, mut f: F) -> Type<'b>
|
||||
where
|
||||
F: FnMut(Ident<'a>) -> Type<'b> + Clone,
|
||||
{
|
||||
match self {
|
||||
Type::Var(tv) => f(tv),
|
||||
Type::Function(FunctionType { args, ret }) => Type::Function(FunctionType {
|
||||
args: args
|
||||
.into_iter()
|
||||
.map(|t| t.traverse_type_vars(f.clone()))
|
||||
.collect(),
|
||||
ret: Box::new(ret.traverse_type_vars(f)),
|
||||
}),
|
||||
Type::Int => Type::Int,
|
||||
Type::Float => Type::Float,
|
||||
Type::Bool => Type::Bool,
|
||||
Type::CString => Type::CString,
|
||||
Type::Tuple(members) => Type::Tuple(
|
||||
members
|
||||
.into_iter()
|
||||
.map(|t| t.traverse_type_vars(f.clone()))
|
||||
.collect(),
|
||||
),
|
||||
Type::Unit => Type::Unit,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_tuple(&self) -> Option<&Vec<Type<'a>>> {
|
||||
if let Self::Tuple(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Type<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Type::Int => f.write_str("int"),
|
||||
Type::Float => f.write_str("float"),
|
||||
Type::Bool => f.write_str("bool"),
|
||||
Type::CString => f.write_str("cstring"),
|
||||
Type::Unit => f.write_str("()"),
|
||||
Type::Var(v) => v.fmt(f),
|
||||
Type::Function(ft) => ft.fmt(f),
|
||||
Type::Tuple(ms) => write!(f, "({})", ms.iter().join(", ")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn type_var(n: &str) -> Type<'static> {
|
||||
Type::Var(Ident::try_from(n.to_owned()).unwrap())
|
||||
}
|
||||
|
||||
mod alpha_equiv {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn trivial() {
|
||||
assert!(Type::Int.alpha_equiv(&Type::Int));
|
||||
assert!(!Type::Int.alpha_equiv(&Type::Bool));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_type_var() {
|
||||
assert!(type_var("a").alpha_equiv(&type_var("b")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_with_type_vars_equiv() {
|
||||
assert!(Type::Function(FunctionType {
|
||||
args: vec![type_var("a")],
|
||||
ret: Box::new(type_var("b")),
|
||||
})
|
||||
.alpha_equiv(&Type::Function(FunctionType {
|
||||
args: vec![type_var("b")],
|
||||
ret: Box::new(type_var("a")),
|
||||
})))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_with_type_vars_non_equiv() {
|
||||
assert!(!Type::Function(FunctionType {
|
||||
args: vec![type_var("a")],
|
||||
ret: Box::new(type_var("a")),
|
||||
})
|
||||
.alpha_equiv(&Type::Function(FunctionType {
|
||||
args: vec![type_var("b")],
|
||||
ret: Box::new(type_var("a")),
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
486
users/aspen/achilles/src/codegen/llvm.rs
Normal file
486
users/aspen/achilles/src/codegen/llvm.rs
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
use std::convert::{TryFrom, TryInto};
|
||||
use std::path::Path;
|
||||
use std::result;
|
||||
|
||||
use inkwell::basic_block::BasicBlock;
|
||||
use inkwell::builder::Builder;
|
||||
pub use inkwell::context::Context;
|
||||
use inkwell::module::Module;
|
||||
use inkwell::support::LLVMString;
|
||||
use inkwell::types::{BasicType, BasicTypeEnum, FunctionType, IntType, StructType};
|
||||
use inkwell::values::{AnyValueEnum, BasicValueEnum, FunctionValue, StructValue};
|
||||
use inkwell::{AddressSpace, IntPredicate};
|
||||
use itertools::Itertools;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ast::hir::{Binding, Decl, Expr, Pattern};
|
||||
use crate::ast::{BinaryOperator, Ident, Literal, Type, UnaryOperator};
|
||||
use crate::common::env::Env;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Error)]
|
||||
pub enum Error {
|
||||
#[error("Undefined variable {0}")]
|
||||
UndefinedVariable(Ident<'static>),
|
||||
|
||||
#[error("LLVM Error: {0}")]
|
||||
LLVMError(String),
|
||||
}
|
||||
|
||||
impl From<LLVMString> for Error {
|
||||
fn from(s: LLVMString) -> Self {
|
||||
Self::LLVMError(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
||||
pub struct Codegen<'ctx, 'ast> {
|
||||
context: &'ctx Context,
|
||||
pub module: Module<'ctx>,
|
||||
builder: Builder<'ctx>,
|
||||
env: Env<&'ast Ident<'ast>, AnyValueEnum<'ctx>>,
|
||||
function_stack: Vec<FunctionValue<'ctx>>,
|
||||
identifier_counter: u32,
|
||||
}
|
||||
|
||||
impl<'ctx, 'ast> Codegen<'ctx, 'ast> {
|
||||
pub fn new(context: &'ctx Context, module_name: &str) -> Self {
|
||||
let module = context.create_module(module_name);
|
||||
let builder = context.create_builder();
|
||||
Self {
|
||||
context,
|
||||
module,
|
||||
builder,
|
||||
env: Default::default(),
|
||||
function_stack: Default::default(),
|
||||
identifier_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_function<'a>(
|
||||
&'a mut self,
|
||||
name: &str,
|
||||
ty: FunctionType<'ctx>,
|
||||
) -> &'a FunctionValue<'ctx> {
|
||||
self.function_stack
|
||||
.push(self.module.add_function(name, ty, None));
|
||||
let basic_block = self.append_basic_block("entry");
|
||||
self.builder.position_at_end(basic_block);
|
||||
self.function_stack.last().unwrap()
|
||||
}
|
||||
|
||||
pub fn finish_function(&mut self, res: Option<&BasicValueEnum<'ctx>>) -> FunctionValue<'ctx> {
|
||||
self.builder.build_return(match res {
|
||||
// lol
|
||||
Some(val) => Some(val),
|
||||
None => None,
|
||||
});
|
||||
self.function_stack.pop().unwrap()
|
||||
}
|
||||
|
||||
pub fn append_basic_block(&self, name: &str) -> BasicBlock<'ctx> {
|
||||
self.context
|
||||
.append_basic_block(*self.function_stack.last().unwrap(), name)
|
||||
}
|
||||
|
||||
fn bind_pattern(&mut self, pat: &'ast Pattern<'ast, Type>, val: AnyValueEnum<'ctx>) {
|
||||
match pat {
|
||||
Pattern::Id(id, _) => self.env.set(id, val),
|
||||
Pattern::Tuple(pats) => {
|
||||
for (i, pat) in pats.iter().enumerate() {
|
||||
let member = self
|
||||
.builder
|
||||
.build_extract_value(
|
||||
StructValue::try_from(val).unwrap(),
|
||||
i as _,
|
||||
"pat_bind",
|
||||
)
|
||||
.unwrap();
|
||||
self.bind_pattern(pat, member.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codegen_expr(
|
||||
&mut self,
|
||||
expr: &'ast Expr<'ast, Type>,
|
||||
) -> Result<Option<AnyValueEnum<'ctx>>> {
|
||||
match expr {
|
||||
Expr::Ident(id, _) => self
|
||||
.env
|
||||
.resolve(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error::UndefinedVariable(id.to_owned()))
|
||||
.map(Some),
|
||||
Expr::Literal(lit, ty) => {
|
||||
let ty = self.codegen_int_type(ty);
|
||||
match lit {
|
||||
Literal::Int(i) => Ok(Some(AnyValueEnum::IntValue(ty.const_int(*i, false)))),
|
||||
Literal::Bool(b) => Ok(Some(AnyValueEnum::IntValue(
|
||||
ty.const_int(if *b { 1 } else { 0 }, false),
|
||||
))),
|
||||
Literal::String(s) => Ok(Some(
|
||||
self.builder
|
||||
.build_global_string_ptr(s, "s")
|
||||
.as_pointer_value()
|
||||
.into(),
|
||||
)),
|
||||
Literal::Unit => Ok(None),
|
||||
}
|
||||
}
|
||||
Expr::UnaryOp { op, rhs, .. } => {
|
||||
let rhs = self.codegen_expr(rhs)?.unwrap();
|
||||
match op {
|
||||
UnaryOperator::Not => unimplemented!(),
|
||||
UnaryOperator::Neg => Ok(Some(AnyValueEnum::IntValue(
|
||||
self.builder.build_int_neg(rhs.into_int_value(), "neg"),
|
||||
))),
|
||||
}
|
||||
}
|
||||
Expr::BinaryOp { lhs, op, rhs, .. } => {
|
||||
let lhs = self.codegen_expr(lhs)?.unwrap();
|
||||
let rhs = self.codegen_expr(rhs)?.unwrap();
|
||||
match op {
|
||||
BinaryOperator::Add => {
|
||||
Ok(Some(AnyValueEnum::IntValue(self.builder.build_int_add(
|
||||
lhs.into_int_value(),
|
||||
rhs.into_int_value(),
|
||||
"add",
|
||||
))))
|
||||
}
|
||||
BinaryOperator::Sub => {
|
||||
Ok(Some(AnyValueEnum::IntValue(self.builder.build_int_sub(
|
||||
lhs.into_int_value(),
|
||||
rhs.into_int_value(),
|
||||
"add",
|
||||
))))
|
||||
}
|
||||
BinaryOperator::Mul => {
|
||||
Ok(Some(AnyValueEnum::IntValue(self.builder.build_int_sub(
|
||||
lhs.into_int_value(),
|
||||
rhs.into_int_value(),
|
||||
"add",
|
||||
))))
|
||||
}
|
||||
BinaryOperator::Div => Ok(Some(AnyValueEnum::IntValue(
|
||||
self.builder.build_int_signed_div(
|
||||
lhs.into_int_value(),
|
||||
rhs.into_int_value(),
|
||||
"add",
|
||||
),
|
||||
))),
|
||||
BinaryOperator::Pow => unimplemented!(),
|
||||
BinaryOperator::Equ => Ok(Some(AnyValueEnum::IntValue(
|
||||
self.builder.build_int_compare(
|
||||
IntPredicate::EQ,
|
||||
lhs.into_int_value(),
|
||||
rhs.into_int_value(),
|
||||
"eq",
|
||||
),
|
||||
))),
|
||||
BinaryOperator::Neq => todo!(),
|
||||
}
|
||||
}
|
||||
Expr::Let { bindings, body, .. } => {
|
||||
self.env.push();
|
||||
for Binding { pat, body, .. } in bindings {
|
||||
if let Some(val) = self.codegen_expr(body)? {
|
||||
self.bind_pattern(pat, val);
|
||||
}
|
||||
}
|
||||
let res = self.codegen_expr(body);
|
||||
self.env.pop();
|
||||
res
|
||||
}
|
||||
Expr::If {
|
||||
condition,
|
||||
then,
|
||||
else_,
|
||||
type_,
|
||||
} => {
|
||||
let then_block = self.append_basic_block("then");
|
||||
let else_block = self.append_basic_block("else");
|
||||
let join_block = self.append_basic_block("join");
|
||||
let condition = self.codegen_expr(condition)?.unwrap();
|
||||
self.builder.build_conditional_branch(
|
||||
condition.into_int_value(),
|
||||
then_block,
|
||||
else_block,
|
||||
);
|
||||
self.builder.position_at_end(then_block);
|
||||
let then_res = self.codegen_expr(then)?;
|
||||
self.builder.build_unconditional_branch(join_block);
|
||||
|
||||
self.builder.position_at_end(else_block);
|
||||
let else_res = self.codegen_expr(else_)?;
|
||||
self.builder.build_unconditional_branch(join_block);
|
||||
|
||||
self.builder.position_at_end(join_block);
|
||||
if let Some(phi_type) = self.codegen_type(type_) {
|
||||
let phi = self.builder.build_phi(phi_type, "join");
|
||||
phi.add_incoming(&[
|
||||
(
|
||||
&BasicValueEnum::try_from(then_res.unwrap()).unwrap(),
|
||||
then_block,
|
||||
),
|
||||
(
|
||||
&BasicValueEnum::try_from(else_res.unwrap()).unwrap(),
|
||||
else_block,
|
||||
),
|
||||
]);
|
||||
Ok(Some(phi.as_basic_value().into()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Expr::Call { fun, args, .. } => {
|
||||
if let Expr::Ident(id, _) = &**fun {
|
||||
let function = self
|
||||
.module
|
||||
.get_function(id.into())
|
||||
.or_else(|| self.env.resolve(id)?.clone().try_into().ok())
|
||||
.ok_or_else(|| Error::UndefinedVariable(id.to_owned()))?;
|
||||
let args = args
|
||||
.iter()
|
||||
.map(|arg| Ok(self.codegen_expr(arg)?.unwrap().try_into().unwrap()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(self
|
||||
.builder
|
||||
.build_call(function, &args, "call")
|
||||
.try_as_basic_value()
|
||||
.left()
|
||||
.map(|val| val.into()))
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Expr::Fun { args, body, .. } => {
|
||||
let fname = self.fresh_ident("f");
|
||||
let cur_block = self.builder.get_insert_block().unwrap();
|
||||
let env = self.env.save(); // TODO: closures
|
||||
let function = self.codegen_function(&fname, args, body)?;
|
||||
self.builder.position_at_end(cur_block);
|
||||
self.env.restore(env);
|
||||
Ok(Some(function.into()))
|
||||
}
|
||||
Expr::Tuple(members, ty) => {
|
||||
let values = members
|
||||
.into_iter()
|
||||
.map(|expr| self.codegen_expr(expr))
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.filter_map(|x| x)
|
||||
.map(|x| x.try_into().unwrap())
|
||||
.collect_vec();
|
||||
let field_types = ty.as_tuple().unwrap();
|
||||
let tuple_type = self.codegen_tuple_type(field_types);
|
||||
Ok(Some(tuple_type.const_named_struct(&values).into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codegen_function(
|
||||
&mut self,
|
||||
name: &str,
|
||||
args: &'ast [(Ident<'ast>, Type)],
|
||||
body: &'ast Expr<'ast, Type>,
|
||||
) -> Result<FunctionValue<'ctx>> {
|
||||
let arg_types = args
|
||||
.iter()
|
||||
.filter_map(|(_, at)| self.codegen_type(at))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.new_function(
|
||||
name,
|
||||
match self.codegen_type(body.type_()) {
|
||||
Some(ret_ty) => ret_ty.fn_type(&arg_types, false),
|
||||
None => self.context.void_type().fn_type(&arg_types, false),
|
||||
},
|
||||
);
|
||||
self.env.push();
|
||||
for (i, (arg, _)) in args.iter().enumerate() {
|
||||
self.env.set(
|
||||
arg,
|
||||
self.cur_function().get_nth_param(i as u32).unwrap().into(),
|
||||
);
|
||||
}
|
||||
let res = self.codegen_expr(body)?;
|
||||
self.env.pop();
|
||||
Ok(self.finish_function(res.map(|av| av.try_into().unwrap()).as_ref()))
|
||||
}
|
||||
|
||||
pub fn codegen_extern(
|
||||
&mut self,
|
||||
name: &str,
|
||||
args: &'ast [Type],
|
||||
ret: &'ast Type,
|
||||
) -> Result<()> {
|
||||
let arg_types = args
|
||||
.iter()
|
||||
.map(|t| self.codegen_type(t).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
self.module.add_function(
|
||||
name,
|
||||
match self.codegen_type(ret) {
|
||||
Some(ret_ty) => ret_ty.fn_type(&arg_types, false),
|
||||
None => self.context.void_type().fn_type(&arg_types, false),
|
||||
},
|
||||
None,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn codegen_decl(&mut self, decl: &'ast Decl<'ast, Type>) -> Result<()> {
|
||||
match decl {
|
||||
Decl::Fun {
|
||||
name, args, body, ..
|
||||
} => {
|
||||
self.codegen_function(name.into(), args, body)?;
|
||||
Ok(())
|
||||
}
|
||||
Decl::Extern {
|
||||
name,
|
||||
arg_types,
|
||||
ret_type,
|
||||
} => self.codegen_extern(name.into(), arg_types, ret_type),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codegen_main(&mut self, expr: &'ast Expr<'ast, Type>) -> Result<()> {
|
||||
self.new_function("main", self.context.i64_type().fn_type(&[], false));
|
||||
let res = self.codegen_expr(expr)?;
|
||||
if *expr.type_() != Type::Int {
|
||||
self.builder
|
||||
.build_return(Some(&self.context.i64_type().const_int(0, false)));
|
||||
} else {
|
||||
self.finish_function(res.map(|r| r.try_into().unwrap()).as_ref());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn codegen_type(&self, type_: &'ast Type) -> Option<BasicTypeEnum<'ctx>> {
|
||||
// TODO
|
||||
match type_ {
|
||||
Type::Int => Some(self.context.i64_type().into()),
|
||||
Type::Float => Some(self.context.f64_type().into()),
|
||||
Type::Bool => Some(self.context.bool_type().into()),
|
||||
Type::CString => Some(
|
||||
self.context
|
||||
.i8_type()
|
||||
.ptr_type(AddressSpace::Generic)
|
||||
.into(),
|
||||
),
|
||||
Type::Function(_) => todo!(),
|
||||
Type::Var(_) => unreachable!(),
|
||||
Type::Unit => None,
|
||||
Type::Tuple(ts) => Some(self.codegen_tuple_type(ts).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn codegen_tuple_type(&self, ts: &'ast [Type]) -> StructType<'ctx> {
|
||||
self.context.struct_type(
|
||||
ts.iter()
|
||||
.filter_map(|t| self.codegen_type(t))
|
||||
.collect_vec()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn codegen_int_type(&self, type_: &'ast Type) -> IntType<'ctx> {
|
||||
// TODO
|
||||
self.context.i64_type()
|
||||
}
|
||||
|
||||
pub fn print_to_file<P>(&self, path: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
Ok(self.module.print_to_file(path)?)
|
||||
}
|
||||
|
||||
pub fn binary_to_file<P>(&self, path: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
if self.module.write_bitcode_to_path(path.as_ref()) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::LLVMError(
|
||||
"Error writing bitcode to output path".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn fresh_ident(&mut self, prefix: &str) -> String {
|
||||
self.identifier_counter += 1;
|
||||
format!("{}{}", prefix, self.identifier_counter)
|
||||
}
|
||||
|
||||
fn cur_function(&self) -> &FunctionValue<'ctx> {
|
||||
self.function_stack.last().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use inkwell::execution_engine::JitFunction;
|
||||
use inkwell::OptimizationLevel;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn jit_eval<T>(expr: &str) -> anyhow::Result<T> {
|
||||
let expr = crate::parser::expr(expr).unwrap().1;
|
||||
|
||||
let expr = crate::tc::typecheck_expr(expr).unwrap();
|
||||
|
||||
let context = Context::create();
|
||||
let mut codegen = Codegen::new(&context, "test");
|
||||
let execution_engine = codegen
|
||||
.module
|
||||
.create_jit_execution_engine(OptimizationLevel::None)
|
||||
.unwrap();
|
||||
|
||||
codegen.codegen_function("test", &[], &expr)?;
|
||||
|
||||
unsafe {
|
||||
let fun: JitFunction<unsafe extern "C" fn() -> T> =
|
||||
execution_engine.get_function("test")?;
|
||||
Ok(fun.call())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_literals() {
|
||||
assert_eq!(jit_eval::<i64>("1 + 2").unwrap(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_shadowing() {
|
||||
assert_eq!(
|
||||
jit_eval::<i64>("let x = 1 in (let x = 2 in x) + x").unwrap(),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq() {
|
||||
assert_eq!(
|
||||
jit_eval::<i64>("let x = 1 in if x == 1 then 2 else 4").unwrap(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_call() {
|
||||
let res = jit_eval::<i64>("let id = fn x = x in id 1").unwrap();
|
||||
assert_eq!(res, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_tuple_pattern() {
|
||||
let res = jit_eval::<i64>("let (x, y) = (1, 2) in x + y").unwrap();
|
||||
assert_eq!(res, 3);
|
||||
}
|
||||
}
|
||||
25
users/aspen/achilles/src/codegen/mod.rs
Normal file
25
users/aspen/achilles/src/codegen/mod.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
pub mod llvm;
|
||||
|
||||
use inkwell::execution_engine::JitFunction;
|
||||
use inkwell::OptimizationLevel;
|
||||
pub use llvm::*;
|
||||
|
||||
use crate::ast::hir::Expr;
|
||||
use crate::ast::Type;
|
||||
use crate::common::Result;
|
||||
|
||||
pub fn jit_eval<T>(expr: &Expr<Type>) -> Result<T> {
|
||||
let context = Context::create();
|
||||
let mut codegen = Codegen::new(&context, "eval");
|
||||
let execution_engine = codegen
|
||||
.module
|
||||
.create_jit_execution_engine(OptimizationLevel::None)
|
||||
.map_err(Error::from)?;
|
||||
codegen.codegen_function("test", &[], &expr)?;
|
||||
|
||||
unsafe {
|
||||
let fun: JitFunction<unsafe extern "C" fn() -> T> =
|
||||
execution_engine.get_function("eval").unwrap();
|
||||
Ok(fun.call())
|
||||
}
|
||||
}
|
||||
39
users/aspen/achilles/src/commands/check.rs
Normal file
39
users/aspen/achilles/src/commands/check.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use clap::Clap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::ast::Type;
|
||||
use crate::{parser, tc, Result};
|
||||
|
||||
/// Typecheck a file or expression
|
||||
#[derive(Clap)]
|
||||
pub struct Check {
|
||||
/// File to check
|
||||
path: Option<PathBuf>,
|
||||
|
||||
/// Expression to check
|
||||
#[clap(long, short = 'e')]
|
||||
expr: Option<String>,
|
||||
}
|
||||
|
||||
fn run_expr(expr: String) -> Result<Type<'static>> {
|
||||
let (_, parsed) = parser::expr(&expr)?;
|
||||
let hir_expr = tc::typecheck_expr(parsed)?;
|
||||
Ok(hir_expr.type_().to_owned())
|
||||
}
|
||||
|
||||
fn run_path(path: PathBuf) -> Result<Type<'static>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
impl Check {
|
||||
pub fn run(self) -> Result<()> {
|
||||
let type_ = match (self.path, self.expr) {
|
||||
(None, None) => Err("Must specify either a file or expression to check".into()),
|
||||
(Some(_), Some(_)) => Err("Cannot specify both a file and expression to check".into()),
|
||||
(None, Some(expr)) => run_expr(expr),
|
||||
(Some(path), None) => run_path(path),
|
||||
}?;
|
||||
println!("type: {}", type_);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
31
users/aspen/achilles/src/commands/compile.rs
Normal file
31
users/aspen/achilles/src/commands/compile.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::Clap;
|
||||
|
||||
use crate::common::Result;
|
||||
use crate::compiler::{self, CompilerOptions};
|
||||
|
||||
/// Compile a source file
|
||||
#[derive(Clap)]
|
||||
pub struct Compile {
|
||||
/// File to compile
|
||||
file: PathBuf,
|
||||
|
||||
/// Output file
|
||||
#[clap(short = 'o')]
|
||||
out_file: PathBuf,
|
||||
|
||||
#[clap(flatten)]
|
||||
options: CompilerOptions,
|
||||
}
|
||||
|
||||
impl Compile {
|
||||
pub fn run(self) -> Result<()> {
|
||||
eprintln!(
|
||||
">>> {} -> {}",
|
||||
&self.file.to_string_lossy(),
|
||||
self.out_file.to_string_lossy()
|
||||
);
|
||||
compiler::compile_file(&self.file, &self.out_file, &self.options)
|
||||
}
|
||||
}
|
||||
28
users/aspen/achilles/src/commands/eval.rs
Normal file
28
users/aspen/achilles/src/commands/eval.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use clap::Clap;
|
||||
|
||||
use crate::{codegen, interpreter, parser, tc, Result};
|
||||
|
||||
/// Evaluate an expression and print its result
|
||||
#[derive(Clap)]
|
||||
pub struct Eval {
|
||||
/// JIT-compile with LLVM instead of interpreting
|
||||
#[clap(long)]
|
||||
jit: bool,
|
||||
|
||||
/// Expression to evaluate
|
||||
expr: String,
|
||||
}
|
||||
|
||||
impl Eval {
|
||||
pub fn run(self) -> Result<()> {
|
||||
let (_, parsed) = parser::expr(&self.expr)?;
|
||||
let hir = tc::typecheck_expr(parsed)?;
|
||||
let result = if self.jit {
|
||||
codegen::jit_eval::<i64>(&hir)?.into()
|
||||
} else {
|
||||
interpreter::eval(&hir)?
|
||||
};
|
||||
println!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
7
users/aspen/achilles/src/commands/mod.rs
Normal file
7
users/aspen/achilles/src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub mod check;
|
||||
pub mod compile;
|
||||
pub mod eval;
|
||||
|
||||
pub use check::Check;
|
||||
pub use compile::Compile;
|
||||
pub use eval::Eval;
|
||||
59
users/aspen/achilles/src/common/env.rs
Normal file
59
users/aspen/achilles/src/common/env.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::mem;
|
||||
|
||||
/// A lexical environment
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Env<K: Eq + Hash, V>(Vec<HashMap<K, V>>);
|
||||
|
||||
impl<K, V> Default for Env<K, V>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Env<K, V>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self(vec![Default::default()])
|
||||
}
|
||||
|
||||
pub fn push(&mut self) {
|
||||
self.0.push(Default::default());
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) {
|
||||
self.0.pop();
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Self {
|
||||
mem::take(self)
|
||||
}
|
||||
|
||||
pub fn restore(&mut self, saved: Self) {
|
||||
*self = saved;
|
||||
}
|
||||
|
||||
pub fn set(&mut self, k: K, v: V) {
|
||||
self.0.last_mut().unwrap().insert(k, v);
|
||||
}
|
||||
|
||||
pub fn resolve<'a, Q>(&'a self, k: &Q) -> Option<&'a V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
for ctx in self.0.iter().rev() {
|
||||
if let Some(res) = ctx.get(k) {
|
||||
return Some(res);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
59
users/aspen/achilles/src/common/error.rs
Normal file
59
users/aspen/achilles/src/common/error.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use std::{io, result};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{codegen, interpreter, parser, tc};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
IOError(#[from] io::Error),
|
||||
|
||||
#[error("Error parsing input: {0}")]
|
||||
ParseError(#[from] parser::Error),
|
||||
|
||||
#[error("Error evaluating expression: {0}")]
|
||||
EvalError(#[from] interpreter::Error),
|
||||
|
||||
#[error("Compile error: {0}")]
|
||||
CodegenError(#[from] codegen::Error),
|
||||
|
||||
#[error("Type error: {0}")]
|
||||
TypeError(#[from] tc::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Message(String),
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Self::Message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Error {
|
||||
fn from(s: &'a str) -> Self {
|
||||
Self::Message(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<nom::Err<nom::error::Error<&'a str>>> for Error {
|
||||
fn from(e: nom::Err<nom::error::Error<&'a str>>) -> Self {
|
||||
use nom::error::Error as NomError;
|
||||
use nom::Err::*;
|
||||
|
||||
Self::ParseError(match e {
|
||||
Incomplete(i) => Incomplete(i),
|
||||
Error(NomError { input, code }) => Error(NomError {
|
||||
input: input.to_owned(),
|
||||
code,
|
||||
}),
|
||||
Failure(NomError { input, code }) => Failure(NomError {
|
||||
input: input.to_owned(),
|
||||
code,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
6
users/aspen/achilles/src/common/mod.rs
Normal file
6
users/aspen/achilles/src/common/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub(crate) mod env;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod namer;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use namer::{Namer, NamerOf};
|
||||
122
users/aspen/achilles/src/common/namer.rs
Normal file
122
users/aspen/achilles/src/common/namer.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
use std::fmt::Display;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub struct Namer<T, F> {
|
||||
make_name: F,
|
||||
counter: u64,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, F> Namer<T, F> {
|
||||
pub fn new(make_name: F) -> Self {
|
||||
Namer {
|
||||
make_name,
|
||||
counter: 0,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Namer<String, Box<dyn Fn(u64) -> String>> {
|
||||
pub fn with_prefix<T>(prefix: T) -> Self
|
||||
where
|
||||
T: Display + 'static,
|
||||
{
|
||||
Namer::new(move |i| format!("{}{}", prefix, i)).boxed()
|
||||
}
|
||||
|
||||
pub fn with_suffix<T>(suffix: T) -> Self
|
||||
where
|
||||
T: Display + 'static,
|
||||
{
|
||||
Namer::new(move |i| format!("{}{}", i, suffix)).boxed()
|
||||
}
|
||||
|
||||
pub fn alphabetic() -> Self {
|
||||
Namer::new(|i| {
|
||||
if i <= 26 {
|
||||
std::char::from_u32((i + 96) as u32).unwrap().to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}{}",
|
||||
std::char::from_u32(((i % 26) + 96) as u32).unwrap(),
|
||||
i - 26
|
||||
)
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F> Namer<T, F>
|
||||
where
|
||||
F: Fn(u64) -> T,
|
||||
{
|
||||
pub fn make_name(&mut self) -> T {
|
||||
self.counter += 1;
|
||||
(self.make_name)(self.counter)
|
||||
}
|
||||
|
||||
pub fn boxed(self) -> NamerOf<T>
|
||||
where
|
||||
F: 'static,
|
||||
{
|
||||
Namer {
|
||||
make_name: Box::new(self.make_name),
|
||||
counter: self.counter,
|
||||
_phantom: self._phantom,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<G, U>(self, f: G) -> NamerOf<U>
|
||||
where
|
||||
G: Fn(T) -> U + 'static,
|
||||
T: 'static,
|
||||
F: 'static,
|
||||
{
|
||||
Namer {
|
||||
counter: self.counter,
|
||||
make_name: Box::new(move |x| f((self.make_name)(x))),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type NamerOf<T> = Namer<T, Box<dyn Fn(u64) -> T>>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prefix() {
|
||||
let mut namer = Namer::with_prefix("t");
|
||||
assert_eq!(namer.make_name(), "t1");
|
||||
assert_eq!(namer.make_name(), "t2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suffix() {
|
||||
let mut namer = Namer::with_suffix("t");
|
||||
assert_eq!(namer.make_name(), "1t");
|
||||
assert_eq!(namer.make_name(), "2t");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alphabetic() {
|
||||
let mut namer = Namer::alphabetic();
|
||||
assert_eq!(namer.make_name(), "a");
|
||||
assert_eq!(namer.make_name(), "b");
|
||||
(0..25).for_each(|_| {
|
||||
namer.make_name();
|
||||
});
|
||||
assert_eq!(namer.make_name(), "b2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_callback() {
|
||||
let mut namer = Namer::new(|n| n + 1);
|
||||
assert_eq!(namer.make_name(), 2);
|
||||
assert_eq!(namer.make_name(), 3);
|
||||
}
|
||||
}
|
||||
89
users/aspen/achilles/src/compiler.rs
Normal file
89
users/aspen/achilles/src/compiler.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
use std::fmt::{self, Display};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::{fs, result};
|
||||
|
||||
use clap::Clap;
|
||||
use test_strategy::Arbitrary;
|
||||
|
||||
use crate::codegen::{self, Codegen};
|
||||
use crate::common::Result;
|
||||
use crate::passes::hir::{monomorphize, strip_positive_units};
|
||||
use crate::{parser, tc};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Arbitrary)]
|
||||
pub enum OutputFormat {
|
||||
LLVM,
|
||||
Bitcode,
|
||||
}
|
||||
|
||||
impl Default for OutputFormat {
|
||||
fn default() -> Self {
|
||||
Self::Bitcode
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for OutputFormat {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
|
||||
match s {
|
||||
"llvm" => Ok(Self::LLVM),
|
||||
"binary" => Ok(Self::Bitcode),
|
||||
_ => Err(format!(
|
||||
"Invalid output format {}, expected one of {{llvm, binary}}",
|
||||
s
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for OutputFormat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
OutputFormat::LLVM => f.write_str("llvm"),
|
||||
OutputFormat::Bitcode => f.write_str("binary"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clap, Debug, PartialEq, Eq, Default)]
|
||||
pub struct CompilerOptions {
|
||||
#[clap(long, short = 'f', default_value)]
|
||||
format: OutputFormat,
|
||||
}
|
||||
|
||||
pub fn compile_file(input: &Path, output: &Path, options: &CompilerOptions) -> Result<()> {
|
||||
let src = fs::read_to_string(input)?;
|
||||
let (_, decls) = parser::toplevel(&src)?;
|
||||
let mut decls = tc::typecheck_toplevel(decls)?;
|
||||
monomorphize::run_toplevel(&mut decls);
|
||||
strip_positive_units::run_toplevel(&mut decls);
|
||||
|
||||
let context = codegen::Context::create();
|
||||
let mut codegen = Codegen::new(
|
||||
&context,
|
||||
&input
|
||||
.file_stem()
|
||||
.map_or("UNKNOWN".to_owned(), |s| s.to_string_lossy().into_owned()),
|
||||
);
|
||||
for decl in &decls {
|
||||
codegen.codegen_decl(decl)?;
|
||||
}
|
||||
match options.format {
|
||||
OutputFormat::LLVM => codegen.print_to_file(output)?,
|
||||
OutputFormat::Bitcode => codegen.binary_to_file(output)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use test_strategy::proptest;
|
||||
|
||||
#[proptest]
|
||||
fn output_format_display_from_str_round_trip(of: OutputFormat) {
|
||||
assert_eq!(OutputFormat::from_str(&of.to_string()), Ok(of));
|
||||
}
|
||||
}
|
||||
19
users/aspen/achilles/src/interpreter/error.rs
Normal file
19
users/aspen/achilles/src/interpreter/error.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use std::result;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ast::{Ident, Type};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Error)]
|
||||
pub enum Error {
|
||||
#[error("Undefined variable {0}")]
|
||||
UndefinedVariable(Ident<'static>),
|
||||
|
||||
#[error("Unexpected type {actual}, expected type {expected}")]
|
||||
InvalidType {
|
||||
actual: Type<'static>,
|
||||
expected: Type<'static>,
|
||||
},
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
203
users/aspen/achilles/src/interpreter/mod.rs
Normal file
203
users/aspen/achilles/src/interpreter/mod.rs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
mod error;
|
||||
mod value;
|
||||
|
||||
use itertools::Itertools;
|
||||
use value::Val;
|
||||
|
||||
pub use self::error::{Error, Result};
|
||||
pub use self::value::{Function, Value};
|
||||
use crate::ast::hir::{Binding, Expr, Pattern};
|
||||
use crate::ast::{BinaryOperator, FunctionType, Ident, Literal, Type, UnaryOperator};
|
||||
use crate::common::env::Env;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Interpreter<'a> {
|
||||
env: Env<&'a Ident<'a>, Value<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Interpreter<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn resolve(&self, var: &'a Ident<'a>) -> Result<Value<'a>> {
|
||||
self.env
|
||||
.resolve(var)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error::UndefinedVariable(var.to_owned()))
|
||||
}
|
||||
|
||||
fn bind_pattern(&mut self, pattern: &'a Pattern<'a, Type>, value: Value<'a>) {
|
||||
match pattern {
|
||||
Pattern::Id(id, _) => self.env.set(id, value),
|
||||
Pattern::Tuple(pats) => {
|
||||
for (pat, val) in pats.iter().zip(value.as_tuple().unwrap().clone()) {
|
||||
self.bind_pattern(pat, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval(&mut self, expr: &'a Expr<'a, Type>) -> Result<Value<'a>> {
|
||||
let res = match expr {
|
||||
Expr::Ident(id, _) => self.resolve(id),
|
||||
Expr::Literal(Literal::Int(i), _) => Ok((*i).into()),
|
||||
Expr::Literal(Literal::Bool(b), _) => Ok((*b).into()),
|
||||
Expr::Literal(Literal::String(s), _) => Ok(s.clone().into()),
|
||||
Expr::Literal(Literal::Unit, _) => unreachable!(),
|
||||
Expr::UnaryOp { op, rhs, .. } => {
|
||||
let rhs = self.eval(rhs)?;
|
||||
match op {
|
||||
UnaryOperator::Neg => -rhs,
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
Expr::BinaryOp { lhs, op, rhs, .. } => {
|
||||
let lhs = self.eval(lhs)?;
|
||||
let rhs = self.eval(rhs)?;
|
||||
match op {
|
||||
BinaryOperator::Add => lhs + rhs,
|
||||
BinaryOperator::Sub => lhs - rhs,
|
||||
BinaryOperator::Mul => lhs * rhs,
|
||||
BinaryOperator::Div => lhs / rhs,
|
||||
BinaryOperator::Pow => todo!(),
|
||||
BinaryOperator::Equ => Ok(lhs.eq(&rhs).into()),
|
||||
BinaryOperator::Neq => todo!(),
|
||||
}
|
||||
}
|
||||
Expr::Let { bindings, body, .. } => {
|
||||
self.env.push();
|
||||
for Binding { pat, body, .. } in bindings {
|
||||
let val = self.eval(body)?;
|
||||
self.bind_pattern(pat, val);
|
||||
}
|
||||
let res = self.eval(body)?;
|
||||
self.env.pop();
|
||||
Ok(res)
|
||||
}
|
||||
Expr::If {
|
||||
condition,
|
||||
then,
|
||||
else_,
|
||||
..
|
||||
} => {
|
||||
let condition = self.eval(condition)?;
|
||||
if *(condition.as_type::<bool>()?) {
|
||||
self.eval(then)
|
||||
} else {
|
||||
self.eval(else_)
|
||||
}
|
||||
}
|
||||
Expr::Call { ref fun, args, .. } => {
|
||||
let fun = self.eval(fun)?;
|
||||
let expected_type = FunctionType {
|
||||
args: args.iter().map(|_| Type::Int).collect(),
|
||||
ret: Box::new(Type::Int),
|
||||
};
|
||||
|
||||
let Function {
|
||||
args: function_args,
|
||||
body,
|
||||
..
|
||||
} = fun.as_function(expected_type)?;
|
||||
let arg_values = function_args.iter().zip(
|
||||
args.iter()
|
||||
.map(|v| self.eval(v))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
);
|
||||
let mut interpreter = Interpreter::new();
|
||||
for (arg_name, arg_value) in arg_values {
|
||||
interpreter.env.set(arg_name, arg_value);
|
||||
}
|
||||
Ok(Value::from(*interpreter.eval(body)?.as_type::<i64>()?))
|
||||
}
|
||||
Expr::Fun {
|
||||
type_args: _,
|
||||
args,
|
||||
body,
|
||||
type_,
|
||||
} => {
|
||||
let type_ = match type_ {
|
||||
Type::Function(ft) => ft.clone(),
|
||||
_ => unreachable!("Function expression without function type"),
|
||||
};
|
||||
|
||||
Ok(Value::from(value::Function {
|
||||
// TODO
|
||||
type_,
|
||||
args: args.iter().map(|(arg, _)| arg.to_owned()).collect(),
|
||||
body: (**body).to_owned(),
|
||||
}))
|
||||
}
|
||||
Expr::Tuple(members, _) => Ok(Val::Tuple(
|
||||
members
|
||||
.into_iter()
|
||||
.map(|expr| self.eval(expr))
|
||||
.try_collect()?,
|
||||
)
|
||||
.into()),
|
||||
}?;
|
||||
debug_assert_eq!(&res.type_(), expr.type_());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval<'a>(expr: &'a Expr<'a, Type>) -> Result<Value<'a>> {
|
||||
let mut interpreter = Interpreter::new();
|
||||
interpreter.eval(expr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::value::{TypeOf, Val};
|
||||
use super::*;
|
||||
use BinaryOperator::*;
|
||||
|
||||
fn int_lit(i: u64) -> Box<Expr<'static, Type<'static>>> {
|
||||
Box::new(Expr::Literal(Literal::Int(i), Type::Int))
|
||||
}
|
||||
|
||||
fn do_eval<T>(src: &str) -> T
|
||||
where
|
||||
for<'a> &'a T: TryFrom<&'a Val<'a>>,
|
||||
T: Clone + TypeOf,
|
||||
{
|
||||
let expr = crate::parser::expr(src).unwrap().1;
|
||||
let hir = crate::tc::typecheck_expr(expr).unwrap();
|
||||
let res = eval(&hir).unwrap();
|
||||
res.as_type::<T>().unwrap().clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_addition() {
|
||||
let expr = Expr::BinaryOp {
|
||||
lhs: int_lit(1),
|
||||
op: Mul,
|
||||
rhs: int_lit(2),
|
||||
type_: Type::Int,
|
||||
};
|
||||
let res = eval(&expr).unwrap();
|
||||
assert_eq!(*res.as_type::<i64>().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_shadowing() {
|
||||
let res = do_eval::<i64>("let x = 1 in (let x = 2 in x) + x");
|
||||
assert_eq!(res, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_with_equals() {
|
||||
let res = do_eval::<i64>("let x = 1 in if x == 1 then 2 else 4");
|
||||
assert_eq!(res, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn function_call() {
|
||||
let res = do_eval::<i64>("let id = fn x = x in id 1");
|
||||
assert_eq!(res, 1);
|
||||
}
|
||||
}
|
||||
224
users/aspen/achilles/src/interpreter/value.rs
Normal file
224
users/aspen/achilles/src/interpreter/value.rs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{self, Display};
|
||||
use std::ops::{Add, Div, Mul, Neg, Sub};
|
||||
use std::rc::Rc;
|
||||
use std::result;
|
||||
|
||||
use derive_more::{Deref, From, TryInto};
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{Error, Result};
|
||||
use crate::ast::hir::Expr;
|
||||
use crate::ast::{FunctionType, Ident, Type};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Function<'a> {
|
||||
pub type_: FunctionType<'a>,
|
||||
pub args: Vec<Ident<'a>>,
|
||||
pub body: Expr<'a, Type<'a>>,
|
||||
}
|
||||
|
||||
#[derive(From, TryInto)]
|
||||
#[try_into(owned, ref)]
|
||||
pub enum Val<'a> {
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
String(Cow<'a, str>),
|
||||
Tuple(Vec<Value<'a>>),
|
||||
Function(Function<'a>),
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<Val<'a>> for String {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Val<'a>) -> result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Val::String(s) => Ok(s.into_owned()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for Val<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Val::Int(x) => f.debug_tuple("Int").field(x).finish(),
|
||||
Val::Float(x) => f.debug_tuple("Float").field(x).finish(),
|
||||
Val::Bool(x) => f.debug_tuple("Bool").field(x).finish(),
|
||||
Val::String(s) => f.debug_tuple("String").field(s).finish(),
|
||||
Val::Function(Function { type_, .. }) => {
|
||||
f.debug_struct("Function").field("type_", type_).finish()
|
||||
}
|
||||
Val::Tuple(members) => f.debug_tuple("Tuple").field(members).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for Val<'a> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Val::Int(x), Val::Int(y)) => x == y,
|
||||
(Val::Float(x), Val::Float(y)) => x == y,
|
||||
(Val::Bool(x), Val::Bool(y)) => x == y,
|
||||
(Val::Function(_), Val::Function(_)) => false,
|
||||
(_, _) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<u64> for Val<'a> {
|
||||
fn from(i: u64) -> Self {
|
||||
Self::from(i as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Val<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Val::Int(x) => x.fmt(f),
|
||||
Val::Float(x) => x.fmt(f),
|
||||
Val::Bool(x) => x.fmt(f),
|
||||
Val::String(s) => write!(f, "{:?}", s),
|
||||
Val::Function(Function { type_, .. }) => write!(f, "<{}>", type_),
|
||||
Val::Tuple(members) => write!(f, "({})", members.iter().join(", ")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Val<'a> {
|
||||
pub fn type_(&self) -> Type {
|
||||
match self {
|
||||
Val::Int(_) => Type::Int,
|
||||
Val::Float(_) => Type::Float,
|
||||
Val::Bool(_) => Type::Bool,
|
||||
Val::String(_) => Type::CString,
|
||||
Val::Function(Function { type_, .. }) => Type::Function(type_.clone()),
|
||||
Val::Tuple(members) => Type::Tuple(members.iter().map(|expr| expr.type_()).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_type<'b, T>(&'b self) -> Result<&'b T>
|
||||
where
|
||||
T: TypeOf + 'b + Clone,
|
||||
&'b T: TryFrom<&'b Self>,
|
||||
{
|
||||
<&T>::try_from(self).map_err(|_| Error::InvalidType {
|
||||
actual: self.type_().to_owned(),
|
||||
expected: <T as TypeOf>::type_of(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_function<'b>(&'b self, function_type: FunctionType) -> Result<&'b Function<'a>> {
|
||||
match self {
|
||||
Val::Function(f) if f.type_ == function_type => Ok(&f),
|
||||
_ => Err(Error::InvalidType {
|
||||
actual: self.type_().to_owned(),
|
||||
expected: Type::Function(function_type.to_owned()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_tuple(&self) -> Option<&Vec<Value<'a>>> {
|
||||
if let Self::Tuple(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_into_tuple(self) -> result::Result<Vec<Value<'a>>, Self> {
|
||||
if let Self::Tuple(v) = self {
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Deref)]
|
||||
pub struct Value<'a>(Rc<Val<'a>>);
|
||||
|
||||
impl<'a> Display for Value<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Value<'a>
|
||||
where
|
||||
Val<'a>: From<T>,
|
||||
{
|
||||
fn from(x: T) -> Self {
|
||||
Self(Rc::new(x.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Neg for Value<'a> {
|
||||
type Output = Result<Value<'a>>;
|
||||
|
||||
fn neg(self) -> Self::Output {
|
||||
Ok((-self.as_type::<i64>()?).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Add for Value<'a> {
|
||||
type Output = Result<Value<'a>>;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Ok((self.as_type::<i64>()? + rhs.as_type::<i64>()?).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Sub for Value<'a> {
|
||||
type Output = Result<Value<'a>>;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Ok((self.as_type::<i64>()? - rhs.as_type::<i64>()?).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Mul for Value<'a> {
|
||||
type Output = Result<Value<'a>>;
|
||||
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Ok((self.as_type::<i64>()? * rhs.as_type::<i64>()?).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Div for Value<'a> {
|
||||
type Output = Result<Value<'a>>;
|
||||
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Ok((self.as_type::<f64>()? / rhs.as_type::<f64>()?).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TypeOf {
|
||||
fn type_of() -> Type<'static>;
|
||||
}
|
||||
|
||||
impl TypeOf for i64 {
|
||||
fn type_of() -> Type<'static> {
|
||||
Type::Int
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOf for bool {
|
||||
fn type_of() -> Type<'static> {
|
||||
Type::Bool
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOf for f64 {
|
||||
fn type_of() -> Type<'static> {
|
||||
Type::Float
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOf for String {
|
||||
fn type_of() -> Type<'static> {
|
||||
Type::CString
|
||||
}
|
||||
}
|
||||
36
users/aspen/achilles/src/main.rs
Normal file
36
users/aspen/achilles/src/main.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use clap::Clap;
|
||||
|
||||
pub mod ast;
|
||||
pub mod codegen;
|
||||
pub(crate) mod commands;
|
||||
pub(crate) mod common;
|
||||
pub mod compiler;
|
||||
pub mod interpreter;
|
||||
pub(crate) mod passes;
|
||||
#[macro_use]
|
||||
pub mod parser;
|
||||
pub mod tc;
|
||||
|
||||
pub use common::{Error, Result};
|
||||
|
||||
#[derive(Clap)]
|
||||
struct Opts {
|
||||
#[clap(subcommand)]
|
||||
subcommand: Command,
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
enum Command {
|
||||
Eval(commands::Eval),
|
||||
Compile(commands::Compile),
|
||||
Check(commands::Check),
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let opts = Opts::parse();
|
||||
match opts.subcommand {
|
||||
Command::Eval(eval) => Ok(eval.run()?),
|
||||
Command::Compile(compile) => Ok(compile.run()?),
|
||||
Command::Check(check) => Ok(check.run()?),
|
||||
}
|
||||
}
|
||||
717
users/aspen/achilles/src/parser/expr.rs
Normal file
717
users/aspen/achilles/src/parser/expr.rs
Normal file
|
|
@ -0,0 +1,717 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::character::complete::{digit1, multispace0, multispace1};
|
||||
use nom::{
|
||||
alt, call, char, complete, delimited, do_parse, flat_map, many0, map, named, opt, parse_to,
|
||||
preceded, separated_list0, separated_list1, tag, tuple,
|
||||
};
|
||||
use pratt::{Affix, Associativity, PrattParser, Precedence};
|
||||
|
||||
use super::util::comma;
|
||||
use crate::ast::{BinaryOperator, Binding, Expr, Fun, Literal, Pattern, UnaryOperator};
|
||||
use crate::parser::{arg, ident, type_};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TokenTree<'a> {
|
||||
Prefix(UnaryOperator),
|
||||
// Postfix(char),
|
||||
Infix(BinaryOperator),
|
||||
Primary(Expr<'a>),
|
||||
Group(Vec<TokenTree<'a>>),
|
||||
}
|
||||
|
||||
named!(prefix(&str) -> TokenTree, map!(alt!(
|
||||
complete!(char!('-')) => { |_| UnaryOperator::Neg } |
|
||||
complete!(char!('!')) => { |_| UnaryOperator::Not }
|
||||
), TokenTree::Prefix));
|
||||
|
||||
named!(infix(&str) -> TokenTree, map!(alt!(
|
||||
complete!(tag!("==")) => { |_| BinaryOperator::Equ } |
|
||||
complete!(tag!("!=")) => { |_| BinaryOperator::Neq } |
|
||||
complete!(char!('+')) => { |_| BinaryOperator::Add } |
|
||||
complete!(char!('-')) => { |_| BinaryOperator::Sub } |
|
||||
complete!(char!('*')) => { |_| BinaryOperator::Mul } |
|
||||
complete!(char!('/')) => { |_| BinaryOperator::Div } |
|
||||
complete!(char!('^')) => { |_| BinaryOperator::Pow }
|
||||
), TokenTree::Infix));
|
||||
|
||||
named!(primary(&str) -> TokenTree, alt!(
|
||||
do_parse!(
|
||||
multispace0 >>
|
||||
char!('(') >>
|
||||
multispace0 >>
|
||||
group: group >>
|
||||
multispace0 >>
|
||||
char!(')') >>
|
||||
multispace0 >>
|
||||
(TokenTree::Group(group))
|
||||
) |
|
||||
delimited!(multispace0, simple_expr, multispace0) => { |s| TokenTree::Primary(s) }
|
||||
));
|
||||
|
||||
named!(
|
||||
rest(&str) -> Vec<(TokenTree, Vec<TokenTree>, TokenTree)>,
|
||||
many0!(tuple!(
|
||||
infix,
|
||||
delimited!(multispace0, many0!(prefix), multispace0),
|
||||
primary
|
||||
// many0!(postfix)
|
||||
))
|
||||
);
|
||||
|
||||
named!(group(&str) -> Vec<TokenTree>, do_parse!(
|
||||
prefix: many0!(prefix)
|
||||
>> primary: primary
|
||||
// >> postfix: many0!(postfix)
|
||||
>> rest: rest
|
||||
>> ({
|
||||
let mut res = prefix;
|
||||
res.push(primary);
|
||||
// res.append(&mut postfix);
|
||||
for (infix, mut prefix, primary/*, mut postfix*/) in rest {
|
||||
res.push(infix);
|
||||
res.append(&mut prefix);
|
||||
res.push(primary);
|
||||
// res.append(&mut postfix);
|
||||
}
|
||||
res
|
||||
})
|
||||
));
|
||||
|
||||
fn token_tree(i: &str) -> nom::IResult<&str, Vec<TokenTree>> {
|
||||
group(i)
|
||||
}
|
||||
|
||||
struct ExprParser;
|
||||
|
||||
impl<'a, I> PrattParser<I> for ExprParser
|
||||
where
|
||||
I: Iterator<Item = TokenTree<'a>>,
|
||||
{
|
||||
type Error = pratt::NoError;
|
||||
type Input = TokenTree<'a>;
|
||||
type Output = Expr<'a>;
|
||||
|
||||
fn query(&mut self, input: &Self::Input) -> Result<Affix, Self::Error> {
|
||||
use BinaryOperator::*;
|
||||
use UnaryOperator::*;
|
||||
|
||||
Ok(match input {
|
||||
TokenTree::Infix(Add) => Affix::Infix(Precedence(6), Associativity::Left),
|
||||
TokenTree::Infix(Sub) => Affix::Infix(Precedence(6), Associativity::Left),
|
||||
TokenTree::Infix(Mul) => Affix::Infix(Precedence(7), Associativity::Left),
|
||||
TokenTree::Infix(Div) => Affix::Infix(Precedence(7), Associativity::Left),
|
||||
TokenTree::Infix(Pow) => Affix::Infix(Precedence(8), Associativity::Right),
|
||||
TokenTree::Infix(Equ) => Affix::Infix(Precedence(4), Associativity::Right),
|
||||
TokenTree::Infix(Neq) => Affix::Infix(Precedence(4), Associativity::Right),
|
||||
TokenTree::Prefix(Neg) => Affix::Prefix(Precedence(6)),
|
||||
TokenTree::Prefix(Not) => Affix::Prefix(Precedence(6)),
|
||||
TokenTree::Primary(_) => Affix::Nilfix,
|
||||
TokenTree::Group(_) => Affix::Nilfix,
|
||||
})
|
||||
}
|
||||
|
||||
fn primary(&mut self, input: Self::Input) -> Result<Self::Output, Self::Error> {
|
||||
Ok(match input {
|
||||
TokenTree::Primary(expr) => expr,
|
||||
TokenTree::Group(group) => self.parse(&mut group.into_iter()).unwrap(),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
|
||||
fn infix(
|
||||
&mut self,
|
||||
lhs: Self::Output,
|
||||
op: Self::Input,
|
||||
rhs: Self::Output,
|
||||
) -> Result<Self::Output, Self::Error> {
|
||||
let op = match op {
|
||||
TokenTree::Infix(op) => op,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Ok(Expr::BinaryOp {
|
||||
lhs: Box::new(lhs),
|
||||
op,
|
||||
rhs: Box::new(rhs),
|
||||
})
|
||||
}
|
||||
|
||||
fn prefix(&mut self, op: Self::Input, rhs: Self::Output) -> Result<Self::Output, Self::Error> {
|
||||
let op = match op {
|
||||
TokenTree::Prefix(op) => op,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(Expr::UnaryOp {
|
||||
op,
|
||||
rhs: Box::new(rhs),
|
||||
})
|
||||
}
|
||||
|
||||
fn postfix(
|
||||
&mut self,
|
||||
_lhs: Self::Output,
|
||||
_op: Self::Input,
|
||||
) -> Result<Self::Output, Self::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
named!(int(&str) -> Literal, map!(flat_map!(digit1, parse_to!(u64)), Literal::Int));
|
||||
|
||||
named!(bool_(&str) -> Literal, alt!(
|
||||
complete!(tag!("true")) => { |_| Literal::Bool(true) } |
|
||||
complete!(tag!("false")) => { |_| Literal::Bool(false) }
|
||||
));
|
||||
|
||||
fn string_internal(i: &str) -> nom::IResult<&str, Cow<'_, str>, nom::error::Error<&str>> {
|
||||
// TODO(grfn): use String::split_once when that's stable
|
||||
let (s, rem) = if let Some(pos) = i.find('"') {
|
||||
(&i[..pos], &i[(pos + 1)..])
|
||||
} else {
|
||||
return Err(nom::Err::Error(nom::error::Error::new(
|
||||
i,
|
||||
nom::error::ErrorKind::Tag,
|
||||
)));
|
||||
};
|
||||
|
||||
Ok((rem, Cow::Borrowed(s)))
|
||||
}
|
||||
|
||||
named!(string(&str) -> Literal, preceded!(
|
||||
complete!(char!('"')),
|
||||
map!(
|
||||
string_internal,
|
||||
|s| Literal::String(s)
|
||||
)
|
||||
));
|
||||
|
||||
named!(unit(&str) -> Literal, map!(complete!(tag!("()")), |_| Literal::Unit));
|
||||
|
||||
named!(literal(&str) -> Literal, alt!(int | bool_ | string | unit));
|
||||
|
||||
named!(literal_expr(&str) -> Expr, map!(literal, Expr::Literal));
|
||||
|
||||
named!(tuple(&str) -> Expr, do_parse!(
|
||||
complete!(tag!("("))
|
||||
>> multispace0
|
||||
>> fst: expr
|
||||
>> comma
|
||||
>> rest: separated_list0!(
|
||||
comma,
|
||||
expr
|
||||
)
|
||||
>> multispace0
|
||||
>> tag!(")")
|
||||
>> ({
|
||||
let mut members = Vec::with_capacity(rest.len() + 1);
|
||||
members.push(fst);
|
||||
members.append(&mut rest.clone());
|
||||
Expr::Tuple(members)
|
||||
})
|
||||
));
|
||||
|
||||
named!(tuple_pattern(&str) -> Pattern, do_parse!(
|
||||
complete!(tag!("("))
|
||||
>> multispace0
|
||||
>> pats: separated_list0!(
|
||||
comma,
|
||||
pattern
|
||||
)
|
||||
>> multispace0
|
||||
>> tag!(")")
|
||||
>> (Pattern::Tuple(pats))
|
||||
));
|
||||
|
||||
named!(pattern(&str) -> Pattern, alt!(
|
||||
ident => { |id| Pattern::Id(id) } |
|
||||
tuple_pattern
|
||||
));
|
||||
|
||||
named!(binding(&str) -> Binding, do_parse!(
|
||||
multispace0
|
||||
>> pat: pattern
|
||||
>> multispace0
|
||||
>> type_: opt!(preceded!(tuple!(tag!(":"), multispace0), type_))
|
||||
>> multispace0
|
||||
>> char!('=')
|
||||
>> multispace0
|
||||
>> body: expr
|
||||
>> (Binding {
|
||||
pat,
|
||||
type_,
|
||||
body
|
||||
})
|
||||
));
|
||||
|
||||
named!(let_(&str) -> Expr, do_parse!(
|
||||
tag!("let")
|
||||
>> multispace0
|
||||
>> bindings: separated_list1!(alt!(char!(';') | char!('\n')), binding)
|
||||
>> multispace0
|
||||
>> tag!("in")
|
||||
>> multispace0
|
||||
>> body: expr
|
||||
>> (Expr::Let {
|
||||
bindings,
|
||||
body: Box::new(body)
|
||||
})
|
||||
));
|
||||
|
||||
named!(if_(&str) -> Expr, do_parse! (
|
||||
tag!("if")
|
||||
>> multispace0
|
||||
>> condition: expr
|
||||
>> multispace0
|
||||
>> tag!("then")
|
||||
>> multispace0
|
||||
>> then: expr
|
||||
>> multispace0
|
||||
>> tag!("else")
|
||||
>> multispace0
|
||||
>> else_: expr
|
||||
>> (Expr::If {
|
||||
condition: Box::new(condition),
|
||||
then: Box::new(then),
|
||||
else_: Box::new(else_)
|
||||
})
|
||||
));
|
||||
|
||||
named!(ident_expr(&str) -> Expr, map!(ident, Expr::Ident));
|
||||
|
||||
fn ascripted<'a>(
|
||||
p: impl Fn(&'a str) -> nom::IResult<&'a str, Expr, nom::error::Error<&'a str>> + 'a,
|
||||
) -> impl Fn(&'a str) -> nom::IResult<&str, Expr, nom::error::Error<&'a str>> {
|
||||
move |i| {
|
||||
do_parse!(
|
||||
i,
|
||||
expr: p
|
||||
>> multispace0
|
||||
>> complete!(tag!(":"))
|
||||
>> multispace0
|
||||
>> type_: type_
|
||||
>> (Expr::Ascription {
|
||||
expr: Box::new(expr),
|
||||
type_
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
named!(paren_expr(&str) -> Expr,
|
||||
delimited!(complete!(tag!("(")), expr, complete!(tag!(")"))));
|
||||
|
||||
named!(funcref(&str) -> Expr, alt!(
|
||||
ident_expr |
|
||||
tuple |
|
||||
paren_expr
|
||||
));
|
||||
|
||||
named!(no_arg_call(&str) -> Expr, do_parse!(
|
||||
fun: funcref
|
||||
>> complete!(tag!("()"))
|
||||
>> (Expr::Call {
|
||||
fun: Box::new(fun),
|
||||
args: vec![],
|
||||
})
|
||||
));
|
||||
|
||||
named!(fun_expr(&str) -> Expr, do_parse!(
|
||||
tag!("fn")
|
||||
>> multispace1
|
||||
>> args: separated_list0!(multispace1, arg)
|
||||
>> multispace0
|
||||
>> char!('=')
|
||||
>> multispace0
|
||||
>> body: expr
|
||||
>> (Expr::Fun(Box::new(Fun {
|
||||
args,
|
||||
body
|
||||
})))
|
||||
));
|
||||
|
||||
named!(fn_arg(&str) -> Expr, alt!(
|
||||
ident_expr |
|
||||
literal_expr |
|
||||
tuple |
|
||||
paren_expr
|
||||
));
|
||||
|
||||
named!(call_with_args(&str) -> Expr, do_parse!(
|
||||
fun: funcref
|
||||
>> multispace1
|
||||
>> args: separated_list1!(multispace1, fn_arg)
|
||||
>> (Expr::Call {
|
||||
fun: Box::new(fun),
|
||||
args
|
||||
})
|
||||
));
|
||||
|
||||
named!(simple_expr_unascripted(&str) -> Expr, alt!(
|
||||
let_ |
|
||||
if_ |
|
||||
fun_expr |
|
||||
literal_expr |
|
||||
ident_expr |
|
||||
tuple
|
||||
));
|
||||
|
||||
named!(simple_expr(&str) -> Expr, alt!(
|
||||
call!(ascripted(simple_expr_unascripted)) |
|
||||
simple_expr_unascripted
|
||||
));
|
||||
|
||||
named!(pub expr(&str) -> Expr, alt!(
|
||||
no_arg_call |
|
||||
call_with_args |
|
||||
map!(token_tree, |tt| {
|
||||
ExprParser.parse(&mut tt.into_iter()).unwrap()
|
||||
}) |
|
||||
simple_expr
|
||||
));
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{Arg, Ident, Pattern, Type};
|
||||
use std::convert::TryFrom;
|
||||
use BinaryOperator::*;
|
||||
use Expr::{BinaryOp, If, Let, UnaryOp};
|
||||
use UnaryOperator::*;
|
||||
|
||||
pub(crate) fn ident_expr(s: &str) -> Box<Expr> {
|
||||
Box::new(Expr::Ident(Ident::try_from(s).unwrap()))
|
||||
}
|
||||
|
||||
mod operators {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mul_plus() {
|
||||
let (rem, res) = expr("x*y+z").unwrap();
|
||||
assert!(rem.is_empty());
|
||||
assert_eq!(
|
||||
res,
|
||||
BinaryOp {
|
||||
lhs: Box::new(BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: Mul,
|
||||
rhs: ident_expr("y")
|
||||
}),
|
||||
op: Add,
|
||||
rhs: ident_expr("z")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_plus_ws() {
|
||||
let (rem, res) = expr("x * y + z").unwrap();
|
||||
assert!(rem.is_empty(), "non-empty remainder: \"{}\"", rem);
|
||||
assert_eq!(
|
||||
res,
|
||||
BinaryOp {
|
||||
lhs: Box::new(BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: Mul,
|
||||
rhs: ident_expr("y")
|
||||
}),
|
||||
op: Add,
|
||||
rhs: ident_expr("z")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unary() {
|
||||
let (rem, res) = expr("x * -z").unwrap();
|
||||
assert!(rem.is_empty(), "non-empty remainder: \"{}\"", rem);
|
||||
assert_eq!(
|
||||
res,
|
||||
BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: Mul,
|
||||
rhs: Box::new(UnaryOp {
|
||||
op: Neg,
|
||||
rhs: ident_expr("z"),
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_literal() {
|
||||
let (rem, res) = expr("x * 3").unwrap();
|
||||
assert!(rem.is_empty());
|
||||
assert_eq!(
|
||||
res,
|
||||
BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: Mul,
|
||||
rhs: Box::new(Expr::Literal(Literal::Int(3))),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equ() {
|
||||
let res = test_parse!(expr, "x * 7 == 7");
|
||||
assert_eq!(
|
||||
res,
|
||||
BinaryOp {
|
||||
lhs: Box::new(BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: Mul,
|
||||
rhs: Box::new(Expr::Literal(Literal::Int(7)))
|
||||
}),
|
||||
op: Equ,
|
||||
rhs: Box::new(Expr::Literal(Literal::Int(7)))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit() {
|
||||
assert_eq!(test_parse!(expr, "()"), Expr::Literal(Literal::Unit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bools() {
|
||||
assert_eq!(
|
||||
test_parse!(expr, "true"),
|
||||
Expr::Literal(Literal::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
test_parse!(expr, "false"),
|
||||
Expr::Literal(Literal::Bool(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple() {
|
||||
assert_eq!(
|
||||
test_parse!(expr, "(1, \"seven\")"),
|
||||
Expr::Tuple(vec![
|
||||
Expr::Literal(Literal::Int(1)),
|
||||
Expr::Literal(Literal::String(Cow::Borrowed("seven")))
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_string_lit() {
|
||||
assert_eq!(
|
||||
test_parse!(expr, "\"foobar\""),
|
||||
Expr::Literal(Literal::String(Cow::Borrowed("foobar")))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn let_complex() {
|
||||
let res = test_parse!(expr, "let x = 1; y = x * 7 in (x + y) * 4");
|
||||
assert_eq!(
|
||||
res,
|
||||
Let {
|
||||
bindings: vec![
|
||||
Binding {
|
||||
pat: Pattern::Id(Ident::try_from("x").unwrap()),
|
||||
type_: None,
|
||||
body: Expr::Literal(Literal::Int(1))
|
||||
},
|
||||
Binding {
|
||||
pat: Pattern::Id(Ident::try_from("y").unwrap()),
|
||||
type_: None,
|
||||
body: Expr::BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: Mul,
|
||||
rhs: Box::new(Expr::Literal(Literal::Int(7)))
|
||||
}
|
||||
}
|
||||
],
|
||||
body: Box::new(Expr::BinaryOp {
|
||||
lhs: Box::new(Expr::BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: Add,
|
||||
rhs: ident_expr("y"),
|
||||
}),
|
||||
op: Mul,
|
||||
rhs: Box::new(Expr::Literal(Literal::Int(4))),
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_simple() {
|
||||
let res = test_parse!(expr, "if x == 8 then 9 else 20");
|
||||
assert_eq!(
|
||||
res,
|
||||
If {
|
||||
condition: Box::new(BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: Equ,
|
||||
rhs: Box::new(Expr::Literal(Literal::Int(8))),
|
||||
}),
|
||||
then: Box::new(Expr::Literal(Literal::Int(9))),
|
||||
else_: Box::new(Expr::Literal(Literal::Int(20)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_arg_call() {
|
||||
let res = test_parse!(expr, "f()");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Call {
|
||||
fun: ident_expr("f"),
|
||||
args: vec![]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_call() {
|
||||
let res = test_parse!(expr, "f ()");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Call {
|
||||
fun: ident_expr("f"),
|
||||
args: vec![Expr::Literal(Literal::Unit)]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_with_args() {
|
||||
let res = test_parse!(expr, "f x 1");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Call {
|
||||
fun: ident_expr("f"),
|
||||
args: vec![*ident_expr("x"), Expr::Literal(Literal::Int(1))]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_funcref() {
|
||||
let res = test_parse!(expr, "(let x = 1 in x) 2");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Call {
|
||||
fun: Box::new(Expr::Let {
|
||||
bindings: vec![Binding {
|
||||
pat: Pattern::Id(Ident::try_from("x").unwrap()),
|
||||
type_: None,
|
||||
body: Expr::Literal(Literal::Int(1))
|
||||
}],
|
||||
body: ident_expr("x")
|
||||
}),
|
||||
args: vec![Expr::Literal(Literal::Int(2))]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anon_function() {
|
||||
let res = test_parse!(expr, "let id = fn x = x in id 1");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Let {
|
||||
bindings: vec![Binding {
|
||||
pat: Pattern::Id(Ident::try_from("id").unwrap()),
|
||||
type_: None,
|
||||
body: Expr::Fun(Box::new(Fun {
|
||||
args: vec![Arg::try_from("x").unwrap()],
|
||||
body: *ident_expr("x")
|
||||
}))
|
||||
}],
|
||||
body: Box::new(Expr::Call {
|
||||
fun: ident_expr("id"),
|
||||
args: vec![Expr::Literal(Literal::Int(1))],
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple_binding() {
|
||||
let res = test_parse!(expr, "let (x, y) = (1, 2) in x");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Let {
|
||||
bindings: vec![Binding {
|
||||
pat: Pattern::Tuple(vec![
|
||||
Pattern::Id(Ident::from_str_unchecked("x")),
|
||||
Pattern::Id(Ident::from_str_unchecked("y"))
|
||||
]),
|
||||
body: Expr::Tuple(vec![
|
||||
Expr::Literal(Literal::Int(1)),
|
||||
Expr::Literal(Literal::Int(2))
|
||||
]),
|
||||
type_: None
|
||||
}],
|
||||
body: Box::new(Expr::Ident(Ident::from_str_unchecked("x")))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
mod ascriptions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bare_ascription() {
|
||||
let res = test_parse!(expr, "1: float");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Ascription {
|
||||
expr: Box::new(Expr::Literal(Literal::Int(1))),
|
||||
type_: Type::Float
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_body_ascription() {
|
||||
let res = test_parse!(expr, "let const_1 = fn x = 1: int in const_1 2");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Let {
|
||||
bindings: vec![Binding {
|
||||
pat: Pattern::Id(Ident::try_from("const_1").unwrap()),
|
||||
type_: None,
|
||||
body: Expr::Fun(Box::new(Fun {
|
||||
args: vec![Arg::try_from("x").unwrap()],
|
||||
body: Expr::Ascription {
|
||||
expr: Box::new(Expr::Literal(Literal::Int(1))),
|
||||
type_: Type::Int,
|
||||
}
|
||||
}))
|
||||
}],
|
||||
body: Box::new(Expr::Call {
|
||||
fun: ident_expr("const_1"),
|
||||
args: vec![Expr::Literal(Literal::Int(2))]
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn let_binding_ascripted() {
|
||||
let res = test_parse!(expr, "let x: int = 1 in x");
|
||||
assert_eq!(
|
||||
res,
|
||||
Expr::Let {
|
||||
bindings: vec![Binding {
|
||||
pat: Pattern::Id(Ident::try_from("x").unwrap()),
|
||||
type_: Some(Type::Int),
|
||||
body: Expr::Literal(Literal::Int(1))
|
||||
}],
|
||||
body: ident_expr("x")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
16
users/aspen/achilles/src/parser/macros.rs
Normal file
16
users/aspen/achilles/src/parser/macros.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
macro_rules! test_parse {
|
||||
($parser: ident, $src: expr) => {{
|
||||
let res = $parser($src);
|
||||
nom_trace::print_trace!();
|
||||
let (rem, res) = res.unwrap();
|
||||
assert!(
|
||||
rem.is_empty(),
|
||||
"non-empty remainder: \"{}\", parsed: {:?}",
|
||||
rem,
|
||||
res
|
||||
);
|
||||
res
|
||||
}};
|
||||
}
|
||||
240
users/aspen/achilles/src/parser/mod.rs
Normal file
240
users/aspen/achilles/src/parser/mod.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
use nom::character::complete::{multispace0, multispace1};
|
||||
use nom::error::{ErrorKind, ParseError};
|
||||
use nom::{alt, char, complete, do_parse, eof, many0, named, separated_list0, tag, terminated};
|
||||
|
||||
#[macro_use]
|
||||
pub(crate) mod macros;
|
||||
mod expr;
|
||||
mod type_;
|
||||
mod util;
|
||||
|
||||
use crate::ast::{Arg, Decl, Fun, Ident};
|
||||
pub use expr::expr;
|
||||
use type_::function_type;
|
||||
pub use type_::type_;
|
||||
|
||||
pub type Error = nom::Err<nom::error::Error<String>>;
|
||||
|
||||
pub(crate) fn is_reserved(s: &str) -> bool {
|
||||
matches!(
|
||||
s,
|
||||
"if" | "then"
|
||||
| "else"
|
||||
| "let"
|
||||
| "in"
|
||||
| "fn"
|
||||
| "ty"
|
||||
| "int"
|
||||
| "float"
|
||||
| "bool"
|
||||
| "true"
|
||||
| "false"
|
||||
| "cstring"
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn ident<'a, E>(i: &'a str) -> nom::IResult<&'a str, Ident, E>
|
||||
where
|
||||
E: ParseError<&'a str>,
|
||||
{
|
||||
let mut chars = i.chars();
|
||||
if let Some(f) = chars.next() {
|
||||
if f.is_alphabetic() || f == '_' {
|
||||
let mut idx = 1;
|
||||
for c in chars {
|
||||
if !(c.is_alphanumeric() || c == '_') {
|
||||
break;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
let id = &i[..idx];
|
||||
if is_reserved(id) {
|
||||
Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::Satisfy)))
|
||||
} else {
|
||||
Ok((&i[idx..], Ident::from_str_unchecked(id)))
|
||||
}
|
||||
} else {
|
||||
Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::Satisfy)))
|
||||
}
|
||||
} else {
|
||||
Err(nom::Err::Error(E::from_error_kind(i, ErrorKind::Eof)))
|
||||
}
|
||||
}
|
||||
|
||||
named!(ascripted_arg(&str) -> Arg, do_parse!(
|
||||
complete!(char!('(')) >>
|
||||
multispace0 >>
|
||||
ident: ident >>
|
||||
multispace0 >>
|
||||
complete!(char!(':')) >>
|
||||
multispace0 >>
|
||||
type_: type_ >>
|
||||
multispace0 >>
|
||||
complete!(char!(')')) >>
|
||||
(Arg {
|
||||
ident,
|
||||
type_: Some(type_)
|
||||
})
|
||||
));
|
||||
|
||||
named!(arg(&str) -> Arg, alt!(
|
||||
ident => { |ident| Arg {ident, type_: None}} |
|
||||
ascripted_arg
|
||||
));
|
||||
|
||||
named!(extern_decl(&str) -> Decl, do_parse!(
|
||||
complete!(tag!("extern"))
|
||||
>> multispace1
|
||||
>> name: ident
|
||||
>> multispace0
|
||||
>> char!(':')
|
||||
>> multispace0
|
||||
>> type_: function_type
|
||||
>> multispace0
|
||||
>> (Decl::Extern {
|
||||
name,
|
||||
type_
|
||||
})
|
||||
));
|
||||
|
||||
named!(fun_decl(&str) -> Decl, do_parse!(
|
||||
complete!(tag!("fn"))
|
||||
>> multispace1
|
||||
>> name: ident
|
||||
>> multispace1
|
||||
>> args: separated_list0!(multispace1, arg)
|
||||
>> multispace0
|
||||
>> char!('=')
|
||||
>> multispace0
|
||||
>> body: expr
|
||||
>> (Decl::Fun {
|
||||
name,
|
||||
body: Fun {
|
||||
args,
|
||||
body
|
||||
}
|
||||
})
|
||||
));
|
||||
|
||||
named!(ascription_decl(&str) -> Decl, do_parse!(
|
||||
complete!(tag!("ty"))
|
||||
>> multispace1
|
||||
>> name: ident
|
||||
>> multispace0
|
||||
>> complete!(char!(':'))
|
||||
>> multispace0
|
||||
>> type_: type_
|
||||
>> multispace0
|
||||
>> (Decl::Ascription {
|
||||
name,
|
||||
type_
|
||||
})
|
||||
));
|
||||
|
||||
named!(pub decl(&str) -> Decl, alt!(
|
||||
ascription_decl |
|
||||
fun_decl |
|
||||
extern_decl
|
||||
));
|
||||
|
||||
named!(pub toplevel(&str) -> Vec<Decl>, do_parse!(
|
||||
decls: many0!(decl)
|
||||
>> multispace0
|
||||
>> eof!()
|
||||
>> (decls)));
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::ast::{BinaryOperator, Expr, FunctionType, Literal, Type};
|
||||
|
||||
use super::*;
|
||||
use expr::tests::ident_expr;
|
||||
|
||||
#[test]
|
||||
fn fn_decl() {
|
||||
let res = test_parse!(decl, "fn id x = x");
|
||||
assert_eq!(
|
||||
res,
|
||||
Decl::Fun {
|
||||
name: "id".try_into().unwrap(),
|
||||
body: Fun {
|
||||
args: vec!["x".try_into().unwrap()],
|
||||
body: *ident_expr("x"),
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascripted_fn_args() {
|
||||
test_parse!(ascripted_arg, "(x : int)");
|
||||
let res = test_parse!(decl, "fn plus1 (x : int) = x + 1");
|
||||
assert_eq!(
|
||||
res,
|
||||
Decl::Fun {
|
||||
name: "plus1".try_into().unwrap(),
|
||||
body: Fun {
|
||||
args: vec![Arg {
|
||||
ident: "x".try_into().unwrap(),
|
||||
type_: Some(Type::Int),
|
||||
}],
|
||||
body: Expr::BinaryOp {
|
||||
lhs: ident_expr("x"),
|
||||
op: BinaryOperator::Add,
|
||||
rhs: Box::new(Expr::Literal(Literal::Int(1))),
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_decls() {
|
||||
let res = test_parse!(
|
||||
toplevel,
|
||||
"fn id x = x
|
||||
fn plus x y = x + y
|
||||
fn main = plus (id 2) 7"
|
||||
);
|
||||
assert_eq!(res.len(), 3);
|
||||
let res = test_parse!(
|
||||
toplevel,
|
||||
"fn id x = x\nfn plus x y = x + y\nfn main = plus (id 2) 7\n"
|
||||
);
|
||||
assert_eq!(res.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_ascription() {
|
||||
let res = test_parse!(toplevel, "ty id : fn a -> a");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Decl::Ascription {
|
||||
name: "id".try_into().unwrap(),
|
||||
type_: Type::Function(FunctionType {
|
||||
args: vec![Type::Var("a".try_into().unwrap())],
|
||||
ret: Box::new(Type::Var("a".try_into().unwrap()))
|
||||
})
|
||||
}]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_unit() {
|
||||
assert_eq!(
|
||||
test_parse!(decl, "fn g _ = ()"),
|
||||
Decl::Fun {
|
||||
name: "g".try_into().unwrap(),
|
||||
body: Fun {
|
||||
args: vec![Arg {
|
||||
ident: "_".try_into().unwrap(),
|
||||
type_: None,
|
||||
}],
|
||||
body: Expr::Literal(Literal::Unit),
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
152
users/aspen/achilles/src/parser/type_.rs
Normal file
152
users/aspen/achilles/src/parser/type_.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
use nom::character::complete::{multispace0, multispace1};
|
||||
use nom::{alt, delimited, do_parse, map, named, opt, separated_list0, tag, terminated, tuple};
|
||||
|
||||
use super::ident;
|
||||
use super::util::comma;
|
||||
use crate::ast::{FunctionType, Type};
|
||||
|
||||
named!(pub function_type(&str) -> FunctionType, do_parse!(
|
||||
tag!("fn")
|
||||
>> multispace1
|
||||
>> args: map!(opt!(terminated!(separated_list0!(
|
||||
comma,
|
||||
type_
|
||||
), multispace1)), |args| args.unwrap_or_default())
|
||||
>> tag!("->")
|
||||
>> multispace1
|
||||
>> ret: type_
|
||||
>> (FunctionType {
|
||||
args,
|
||||
ret: Box::new(ret)
|
||||
})
|
||||
));
|
||||
|
||||
named!(tuple_type(&str) -> Type, do_parse!(
|
||||
tag!("(")
|
||||
>> multispace0
|
||||
>> fst: type_
|
||||
>> comma
|
||||
>> rest: separated_list0!(
|
||||
comma,
|
||||
type_
|
||||
)
|
||||
>> multispace0
|
||||
>> tag!(")")
|
||||
>> ({
|
||||
let mut members = Vec::with_capacity(rest.len() + 1);
|
||||
members.push(fst);
|
||||
members.append(&mut rest.clone());
|
||||
Type::Tuple(members)
|
||||
})
|
||||
));
|
||||
|
||||
named!(pub type_(&str) -> Type, alt!(
|
||||
tag!("int") => { |_| Type::Int } |
|
||||
tag!("float") => { |_| Type::Float } |
|
||||
tag!("bool") => { |_| Type::Bool } |
|
||||
tag!("cstring") => { |_| Type::CString } |
|
||||
tag!("()") => { |_| Type::Unit } |
|
||||
tuple_type |
|
||||
function_type => { |ft| Type::Function(ft) }|
|
||||
ident => { |id| Type::Var(id) } |
|
||||
delimited!(
|
||||
tuple!(tag!("("), multispace0),
|
||||
type_,
|
||||
tuple!(tag!(")"), multispace0)
|
||||
)
|
||||
));
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::*;
|
||||
use crate::ast::Ident;
|
||||
|
||||
#[test]
|
||||
fn simple_types() {
|
||||
assert_eq!(test_parse!(type_, "int"), Type::Int);
|
||||
assert_eq!(test_parse!(type_, "float"), Type::Float);
|
||||
assert_eq!(test_parse!(type_, "bool"), Type::Bool);
|
||||
assert_eq!(test_parse!(type_, "cstring"), Type::CString);
|
||||
assert_eq!(test_parse!(type_, "()"), Type::Unit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_arg_fn_type() {
|
||||
assert_eq!(
|
||||
test_parse!(type_, "fn -> int"),
|
||||
Type::Function(FunctionType {
|
||||
args: vec![],
|
||||
ret: Box::new(Type::Int)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_type_with_args() {
|
||||
assert_eq!(
|
||||
test_parse!(type_, "fn int, bool -> int"),
|
||||
Type::Function(FunctionType {
|
||||
args: vec![Type::Int, Type::Bool],
|
||||
ret: Box::new(Type::Int)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_taking_fn() {
|
||||
assert_eq!(
|
||||
test_parse!(type_, "fn fn int, bool -> bool, float -> float"),
|
||||
Type::Function(FunctionType {
|
||||
args: vec![
|
||||
Type::Function(FunctionType {
|
||||
args: vec![Type::Int, Type::Bool],
|
||||
ret: Box::new(Type::Bool)
|
||||
}),
|
||||
Type::Float
|
||||
],
|
||||
ret: Box::new(Type::Float)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parenthesized() {
|
||||
assert_eq!(
|
||||
test_parse!(type_, "fn (fn int, bool -> bool), float -> float"),
|
||||
Type::Function(FunctionType {
|
||||
args: vec![
|
||||
Type::Function(FunctionType {
|
||||
args: vec![Type::Int, Type::Bool],
|
||||
ret: Box::new(Type::Bool)
|
||||
}),
|
||||
Type::Float
|
||||
],
|
||||
ret: Box::new(Type::Float)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple() {
|
||||
assert_eq!(
|
||||
test_parse!(type_, "(int, int)"),
|
||||
Type::Tuple(vec![Type::Int, Type::Int])
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_vars() {
|
||||
assert_eq!(
|
||||
test_parse!(type_, "fn x, y -> x"),
|
||||
Type::Function(FunctionType {
|
||||
args: vec![
|
||||
Type::Var(Ident::try_from("x").unwrap()),
|
||||
Type::Var(Ident::try_from("y").unwrap()),
|
||||
],
|
||||
ret: Box::new(Type::Var(Ident::try_from("x").unwrap())),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
8
users/aspen/achilles/src/parser/util.rs
Normal file
8
users/aspen/achilles/src/parser/util.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
use nom::character::complete::multispace0;
|
||||
use nom::{complete, map, named, tag, tuple};
|
||||
|
||||
named!(pub(crate) comma(&str) -> (), map!(tuple!(
|
||||
multispace0,
|
||||
complete!(tag!(",")),
|
||||
multispace0
|
||||
) ,|_| ()));
|
||||
211
users/aspen/achilles/src/passes/hir/mod.rs
Normal file
211
users/aspen/achilles/src/passes/hir/mod.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::ast::hir::{Binding, Decl, Expr, Pattern};
|
||||
use crate::ast::{BinaryOperator, Ident, Literal, UnaryOperator};
|
||||
|
||||
pub(crate) mod monomorphize;
|
||||
pub(crate) mod strip_positive_units;
|
||||
|
||||
pub(crate) trait Visitor<'a, 'ast, T: 'ast>: Sized + 'a {
|
||||
type Error;
|
||||
|
||||
fn visit_type(&mut self, _type: &mut T) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_ident(&mut self, _ident: &mut Ident<'ast>) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_literal(&mut self, _literal: &mut Literal<'ast>) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_unary_operator(&mut self, _op: &mut UnaryOperator) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_binary_operator(&mut self, _op: &mut BinaryOperator) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, _pat: &mut Pattern<'ast, T>) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_binding(&mut self, binding: &mut Binding<'ast, T>) -> Result<(), Self::Error> {
|
||||
self.visit_pattern(&mut binding.pat)?;
|
||||
self.visit_expr(&mut binding.body)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_visit_call(
|
||||
&mut self,
|
||||
_fun: &mut Expr<'ast, T>,
|
||||
_type_args: &mut HashMap<Ident<'ast>, T>,
|
||||
_args: &mut Vec<Expr<'ast, T>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pre_visit_call(
|
||||
&mut self,
|
||||
_fun: &mut Expr<'ast, T>,
|
||||
_type_args: &mut HashMap<Ident<'ast>, T>,
|
||||
_args: &mut Vec<Expr<'ast, T>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_tuple(&mut self, members: &mut Vec<Expr<'ast, T>>) -> Result<(), Self::Error> {
|
||||
for expr in members {
|
||||
self.visit_expr(expr)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pre_visit_expr(&mut self, _expr: &mut Expr<'ast, T>) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &mut Expr<'ast, T>) -> Result<(), Self::Error> {
|
||||
self.pre_visit_expr(expr)?;
|
||||
match expr {
|
||||
Expr::Ident(id, t) => {
|
||||
self.visit_ident(id)?;
|
||||
self.visit_type(t)?;
|
||||
}
|
||||
Expr::Literal(lit, t) => {
|
||||
self.visit_literal(lit)?;
|
||||
self.visit_type(t)?;
|
||||
}
|
||||
Expr::UnaryOp { op, rhs, type_ } => {
|
||||
self.visit_unary_operator(op)?;
|
||||
self.visit_expr(rhs)?;
|
||||
self.visit_type(type_)?;
|
||||
}
|
||||
Expr::BinaryOp {
|
||||
lhs,
|
||||
op,
|
||||
rhs,
|
||||
type_,
|
||||
} => {
|
||||
self.visit_expr(lhs)?;
|
||||
self.visit_binary_operator(op)?;
|
||||
self.visit_expr(rhs)?;
|
||||
self.visit_type(type_)?;
|
||||
}
|
||||
Expr::Let {
|
||||
bindings,
|
||||
body,
|
||||
type_,
|
||||
} => {
|
||||
for binding in bindings.iter_mut() {
|
||||
self.visit_binding(binding)?;
|
||||
}
|
||||
self.visit_expr(body)?;
|
||||
self.visit_type(type_)?;
|
||||
}
|
||||
Expr::If {
|
||||
condition,
|
||||
then,
|
||||
else_,
|
||||
type_,
|
||||
} => {
|
||||
self.visit_expr(condition)?;
|
||||
self.visit_expr(then)?;
|
||||
self.visit_expr(else_)?;
|
||||
self.visit_type(type_)?;
|
||||
}
|
||||
Expr::Fun {
|
||||
args,
|
||||
body,
|
||||
type_args,
|
||||
type_,
|
||||
} => {
|
||||
for (ident, t) in args {
|
||||
self.visit_ident(ident)?;
|
||||
self.visit_type(t)?;
|
||||
}
|
||||
for ta in type_args {
|
||||
self.visit_ident(ta)?;
|
||||
}
|
||||
self.visit_expr(body)?;
|
||||
self.visit_type(type_)?;
|
||||
}
|
||||
Expr::Call {
|
||||
fun,
|
||||
args,
|
||||
type_args,
|
||||
type_,
|
||||
} => {
|
||||
self.pre_visit_call(fun, type_args, args)?;
|
||||
self.visit_expr(fun)?;
|
||||
for arg in args.iter_mut() {
|
||||
self.visit_expr(arg)?;
|
||||
}
|
||||
self.visit_type(type_)?;
|
||||
self.post_visit_call(fun, type_args, args)?;
|
||||
}
|
||||
Expr::Tuple(tup, type_) => {
|
||||
self.visit_tuple(tup)?;
|
||||
self.visit_type(type_)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_visit_decl(&mut self, _decl: &'a Decl<'ast, T>) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_visit_fun_decl(
|
||||
&mut self,
|
||||
_name: &mut Ident<'ast>,
|
||||
_type_args: &mut Vec<Ident>,
|
||||
_args: &mut Vec<(Ident, T)>,
|
||||
_body: &mut Box<Expr<T>>,
|
||||
_type_: &mut T,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_decl(&mut self, decl: &'a mut Decl<'ast, T>) -> Result<(), Self::Error> {
|
||||
match decl {
|
||||
Decl::Fun {
|
||||
name,
|
||||
type_args,
|
||||
args,
|
||||
body,
|
||||
type_,
|
||||
} => {
|
||||
self.visit_ident(name)?;
|
||||
for type_arg in type_args.iter_mut() {
|
||||
self.visit_ident(type_arg)?;
|
||||
}
|
||||
for (arg, t) in args.iter_mut() {
|
||||
self.visit_ident(arg)?;
|
||||
self.visit_type(t)?;
|
||||
}
|
||||
self.visit_expr(body)?;
|
||||
self.visit_type(type_)?;
|
||||
self.post_visit_fun_decl(name, type_args, args, body, type_)?;
|
||||
}
|
||||
Decl::Extern {
|
||||
name,
|
||||
arg_types,
|
||||
ret_type,
|
||||
} => {
|
||||
self.visit_ident(name)?;
|
||||
for arg_t in arg_types {
|
||||
self.visit_type(arg_t)?;
|
||||
}
|
||||
self.visit_type(ret_type)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.post_visit_decl(decl)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
139
users/aspen/achilles/src/passes/hir/monomorphize.rs
Normal file
139
users/aspen/achilles/src/passes/hir/monomorphize.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::TryInto;
|
||||
use std::mem;
|
||||
|
||||
use void::{ResultVoidExt, Void};
|
||||
|
||||
use crate::ast::hir::{Decl, Expr};
|
||||
use crate::ast::{self, Ident};
|
||||
|
||||
use super::Visitor;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Monomorphize<'a, 'ast> {
|
||||
decls: HashMap<&'a Ident<'ast>, &'a Decl<'ast, ast::Type<'ast>>>,
|
||||
extra_decls: Vec<Decl<'ast, ast::Type<'ast>>>,
|
||||
remove_decls: HashSet<Ident<'ast>>,
|
||||
}
|
||||
|
||||
impl<'a, 'ast> Monomorphize<'a, 'ast> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'ast> Visitor<'a, 'ast, ast::Type<'ast>> for Monomorphize<'a, 'ast> {
|
||||
type Error = Void;
|
||||
|
||||
fn post_visit_call(
|
||||
&mut self,
|
||||
fun: &mut Expr<'ast, ast::Type<'ast>>,
|
||||
type_args: &mut HashMap<Ident<'ast>, ast::Type<'ast>>,
|
||||
args: &mut Vec<Expr<'ast, ast::Type<'ast>>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let new_fun = match fun {
|
||||
Expr::Ident(id, _) => {
|
||||
let decl: Decl<_> = (**self.decls.get(id).unwrap()).clone();
|
||||
let name = RefCell::new(id.to_string());
|
||||
let type_args = mem::take(type_args);
|
||||
let mut monomorphized = decl
|
||||
.traverse_type(|ty| -> Result<_, Void> {
|
||||
Ok(ty.clone().traverse_type_vars(|v| {
|
||||
let concrete = type_args.get(&v).unwrap();
|
||||
name.borrow_mut().push_str(&concrete.to_string());
|
||||
concrete.clone()
|
||||
}))
|
||||
})
|
||||
.void_unwrap();
|
||||
let name: Ident = name.into_inner().try_into().unwrap();
|
||||
if name != *id {
|
||||
self.remove_decls.insert(id.clone());
|
||||
monomorphized.set_name(name.clone());
|
||||
let type_ = monomorphized.type_().unwrap().clone();
|
||||
self.extra_decls.push(monomorphized);
|
||||
Some(Expr::Ident(name, type_))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => todo!(),
|
||||
};
|
||||
if let Some(new_fun) = new_fun {
|
||||
*fun = new_fun;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_visit_decl(
|
||||
&mut self,
|
||||
decl: &'a Decl<'ast, ast::Type<'ast>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.decls.insert(decl.name(), decl);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_toplevel<'a>(toplevel: &mut Vec<Decl<'a, ast::Type<'a>>>) {
|
||||
let mut pass = Monomorphize::new();
|
||||
for decl in toplevel.iter_mut() {
|
||||
pass.visit_decl(decl).void_unwrap();
|
||||
}
|
||||
let remove_decls = mem::take(&mut pass.remove_decls);
|
||||
let mut extra_decls = mem::take(&mut pass.extra_decls);
|
||||
toplevel.retain(|decl| !remove_decls.contains(decl.name()));
|
||||
extra_decls.append(toplevel);
|
||||
*toplevel = extra_decls;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::*;
|
||||
use crate::parser::toplevel;
|
||||
use crate::tc::typecheck_toplevel;
|
||||
|
||||
#[test]
|
||||
fn call_id_decl() {
|
||||
let (_, program) = toplevel(
|
||||
"ty id : fn a -> a
|
||||
fn id x = x
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = id 0",
|
||||
)
|
||||
.unwrap();
|
||||
let mut program = typecheck_toplevel(program).unwrap();
|
||||
run_toplevel(&mut program);
|
||||
|
||||
let find_decl = |ident: &str| {
|
||||
program.iter().find(|decl| {
|
||||
matches!(decl, Decl::Fun {name, ..} if name == &Ident::try_from(ident).unwrap())
|
||||
}).unwrap()
|
||||
};
|
||||
|
||||
let main = find_decl("main");
|
||||
let body = match main {
|
||||
Decl::Fun { body, .. } => body,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let expected_type = ast::Type::Function(ast::FunctionType {
|
||||
args: vec![ast::Type::Int],
|
||||
ret: Box::new(ast::Type::Int),
|
||||
});
|
||||
|
||||
match &**body {
|
||||
Expr::Call { fun, .. } => {
|
||||
let fun = match &**fun {
|
||||
Expr::Ident(fun, _) => fun,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let called_decl = find_decl(fun.into());
|
||||
assert_eq!(called_decl.type_().unwrap(), &expected_type);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
191
users/aspen/achilles/src/passes/hir/strip_positive_units.rs
Normal file
191
users/aspen/achilles/src/passes/hir/strip_positive_units.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
|
||||
use ast::hir::{Binding, Pattern};
|
||||
use ast::Literal;
|
||||
use void::{ResultVoidExt, Void};
|
||||
|
||||
use crate::ast::hir::{Decl, Expr};
|
||||
use crate::ast::{self, Ident};
|
||||
|
||||
use super::Visitor;
|
||||
|
||||
/// Strip all values with a unit type in positive (non-return) position
|
||||
pub(crate) struct StripPositiveUnits {}
|
||||
|
||||
impl<'a, 'ast> Visitor<'a, 'ast, ast::Type<'ast>> for StripPositiveUnits {
|
||||
type Error = Void;
|
||||
|
||||
fn pre_visit_expr(
|
||||
&mut self,
|
||||
expr: &mut Expr<'ast, ast::Type<'ast>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut extracted = vec![];
|
||||
if let Expr::Call { args, .. } = expr {
|
||||
// TODO(grfn): replace with drain_filter once it's stabilized
|
||||
let mut i = 0;
|
||||
while i != args.len() {
|
||||
if args[i].type_() == &ast::Type::Unit {
|
||||
let expr = args.remove(i);
|
||||
if !matches!(expr, Expr::Literal(Literal::Unit, _)) {
|
||||
extracted.push(expr)
|
||||
};
|
||||
} else {
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !extracted.is_empty() {
|
||||
let body = mem::replace(expr, Expr::Literal(Literal::Unit, ast::Type::Unit));
|
||||
*expr = Expr::Let {
|
||||
bindings: extracted
|
||||
.into_iter()
|
||||
.map(|expr| Binding {
|
||||
pat: Pattern::Id(
|
||||
Ident::from_str_unchecked("___discarded"),
|
||||
expr.type_().clone(),
|
||||
),
|
||||
body: expr,
|
||||
})
|
||||
.collect(),
|
||||
type_: body.type_().clone(),
|
||||
body: Box::new(body),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_visit_call(
|
||||
&mut self,
|
||||
_fun: &mut Expr<'ast, ast::Type<'ast>>,
|
||||
_type_args: &mut HashMap<Ident<'ast>, ast::Type<'ast>>,
|
||||
args: &mut Vec<Expr<'ast, ast::Type<'ast>>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
args.retain(|arg| arg.type_() != &ast::Type::Unit);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_type(&mut self, type_: &mut ast::Type<'ast>) -> Result<(), Self::Error> {
|
||||
if let ast::Type::Function(ft) = type_ {
|
||||
ft.args.retain(|a| a != &ast::Type::Unit);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_visit_fun_decl(
|
||||
&mut self,
|
||||
_name: &mut Ident<'ast>,
|
||||
_type_args: &mut Vec<Ident>,
|
||||
args: &mut Vec<(Ident, ast::Type<'ast>)>,
|
||||
_body: &mut Box<Expr<ast::Type<'ast>>>,
|
||||
_type_: &mut ast::Type<'ast>,
|
||||
) -> Result<(), Self::Error> {
|
||||
args.retain(|(_, ty)| ty != &ast::Type::Unit);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_toplevel<'a>(toplevel: &mut Vec<Decl<'a, ast::Type<'a>>>) {
|
||||
let mut pass = StripPositiveUnits {};
|
||||
for decl in toplevel.iter_mut() {
|
||||
pass.visit_decl(decl).void_unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::parser::toplevel;
|
||||
use crate::tc::typecheck_toplevel;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn unit_only_arg() {
|
||||
let (_, program) = toplevel(
|
||||
"ty f : fn () -> int
|
||||
fn f _ = 1
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = f ()",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_, expected) = toplevel(
|
||||
"ty f : fn -> int
|
||||
fn f = 1
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = f()",
|
||||
)
|
||||
.unwrap();
|
||||
let expected = typecheck_toplevel(expected).unwrap();
|
||||
|
||||
let mut program = typecheck_toplevel(program).unwrap();
|
||||
run_toplevel(&mut program);
|
||||
|
||||
assert_eq!(program, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_and_other_arg() {
|
||||
let (_, program) = toplevel(
|
||||
"ty f : fn (), int -> int
|
||||
fn f _ x = x
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = f () 1",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_, expected) = toplevel(
|
||||
"ty f : fn int -> int
|
||||
fn f x = x
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = f 1",
|
||||
)
|
||||
.unwrap();
|
||||
let expected = typecheck_toplevel(expected).unwrap();
|
||||
|
||||
let mut program = typecheck_toplevel(program).unwrap();
|
||||
run_toplevel(&mut program);
|
||||
|
||||
assert_eq!(program, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_expr_and_other_arg() {
|
||||
let (_, program) = toplevel(
|
||||
"ty f : fn (), int -> int
|
||||
fn f _ x = x
|
||||
|
||||
ty g : fn int -> ()
|
||||
fn g _ = ()
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = f (g 2) 1",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_, expected) = toplevel(
|
||||
"ty f : fn int -> int
|
||||
fn f x = x
|
||||
|
||||
ty g : fn int -> ()
|
||||
fn g _ = ()
|
||||
|
||||
ty main : fn -> int
|
||||
fn main = let ___discarded = g 2 in f 1",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(expected.len(), 6);
|
||||
let expected = typecheck_toplevel(expected).unwrap();
|
||||
|
||||
let mut program = typecheck_toplevel(program).unwrap();
|
||||
run_toplevel(&mut program);
|
||||
|
||||
assert_eq!(program, expected);
|
||||
}
|
||||
}
|
||||
1
users/aspen/achilles/src/passes/mod.rs
Normal file
1
users/aspen/achilles/src/passes/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub(crate) mod hir;
|
||||
808
users/aspen/achilles/src/tc/mod.rs
Normal file
808
users/aspen/achilles/src/tc/mod.rs
Normal file
|
|
@ -0,0 +1,808 @@
|
|||
use bimap::BiMap;
|
||||
use derive_more::From;
|
||||
use itertools::Itertools;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt::{self, Display};
|
||||
use std::{mem, result};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ast::{self, hir, Arg, BinaryOperator, Ident, Literal, Pattern};
|
||||
use crate::common::env::Env;
|
||||
use crate::common::{Namer, NamerOf};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Undefined variable {0}")]
|
||||
UndefinedVariable(Ident<'static>),
|
||||
|
||||
#[error("Mismatched types: expected {expected}, but got {actual}")]
|
||||
TypeMismatch { expected: Type, actual: Type },
|
||||
|
||||
#[error("Mismatched types, expected numeric type, but got {0}")]
|
||||
NonNumeric(Type),
|
||||
|
||||
#[error("Ambiguous type {0}")]
|
||||
AmbiguousType(TyVar),
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub struct TyVar(u64);
|
||||
|
||||
impl Display for TyVar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "t{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub struct NullaryType(String);
|
||||
|
||||
impl Display for NullaryType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum PrimType {
|
||||
Int,
|
||||
Float,
|
||||
Bool,
|
||||
CString,
|
||||
}
|
||||
|
||||
impl<'a> From<PrimType> for ast::Type<'a> {
|
||||
fn from(pr: PrimType) -> Self {
|
||||
match pr {
|
||||
PrimType::Int => ast::Type::Int,
|
||||
PrimType::Float => ast::Type::Float,
|
||||
PrimType::Bool => ast::Type::Bool,
|
||||
PrimType::CString => ast::Type::CString,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PrimType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PrimType::Int => f.write_str("int"),
|
||||
PrimType::Float => f.write_str("float"),
|
||||
PrimType::Bool => f.write_str("bool"),
|
||||
PrimType::CString => f.write_str("cstring"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, From)]
|
||||
pub enum Type {
|
||||
#[from(ignore)]
|
||||
Univ(TyVar),
|
||||
#[from(ignore)]
|
||||
Exist(TyVar),
|
||||
Nullary(NullaryType),
|
||||
Prim(PrimType),
|
||||
Tuple(Vec<Type>),
|
||||
Unit,
|
||||
Fun {
|
||||
args: Vec<Type>,
|
||||
ret: Box<Type>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<Type> for ast::Type<'a> {
|
||||
type Error = Type;
|
||||
|
||||
fn try_from(value: Type) -> result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Type::Unit => Ok(ast::Type::Unit),
|
||||
Type::Univ(_) => todo!(),
|
||||
Type::Exist(_) => Err(value),
|
||||
Type::Nullary(_) => todo!(),
|
||||
Type::Prim(p) => Ok(p.into()),
|
||||
Type::Tuple(members) => Ok(ast::Type::Tuple(
|
||||
members.into_iter().map(|ty| ty.try_into()).try_collect()?,
|
||||
)),
|
||||
Type::Fun { ref args, ref ret } => Ok(ast::Type::Function(ast::FunctionType {
|
||||
args: args
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Self::try_from)
|
||||
.try_collect()
|
||||
.map_err(|_| value.clone())?,
|
||||
ret: Box::new((*ret.clone()).try_into().map_err(|_| value.clone())?),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const INT: Type = Type::Prim(PrimType::Int);
|
||||
const FLOAT: Type = Type::Prim(PrimType::Float);
|
||||
const BOOL: Type = Type::Prim(PrimType::Bool);
|
||||
const CSTRING: Type = Type::Prim(PrimType::CString);
|
||||
|
||||
impl Display for Type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Type::Nullary(nt) => nt.fmt(f),
|
||||
Type::Prim(p) => p.fmt(f),
|
||||
Type::Univ(TyVar(n)) => write!(f, "∀{}", n),
|
||||
Type::Exist(TyVar(n)) => write!(f, "∃{}", n),
|
||||
Type::Fun { args, ret } => write!(f, "fn {} -> {}", args.iter().join(", "), ret),
|
||||
Type::Tuple(members) => write!(f, "({})", members.iter().join(", ")),
|
||||
Type::Unit => write!(f, "()"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Typechecker<'ast> {
|
||||
ty_var_namer: NamerOf<TyVar>,
|
||||
ctx: HashMap<TyVar, Type>,
|
||||
env: Env<Ident<'ast>, Type>,
|
||||
|
||||
/// AST type var -> type
|
||||
instantiations: Env<Ident<'ast>, Type>,
|
||||
|
||||
/// AST type-var -> universal TyVar
|
||||
type_vars: RefCell<(BiMap<Ident<'ast>, TyVar>, NamerOf<Ident<'static>>)>,
|
||||
}
|
||||
|
||||
impl<'ast> Typechecker<'ast> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
ty_var_namer: Namer::new(TyVar).boxed(),
|
||||
type_vars: RefCell::new((
|
||||
Default::default(),
|
||||
Namer::alphabetic().map(|n| Ident::try_from(n).unwrap()),
|
||||
)),
|
||||
ctx: Default::default(),
|
||||
env: Default::default(),
|
||||
instantiations: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_pattern(
|
||||
&mut self,
|
||||
pat: Pattern<'ast>,
|
||||
type_: Type,
|
||||
) -> Result<hir::Pattern<'ast, Type>> {
|
||||
match pat {
|
||||
Pattern::Id(ident) => {
|
||||
self.env.set(ident.clone(), type_.clone());
|
||||
Ok(hir::Pattern::Id(ident, type_))
|
||||
}
|
||||
Pattern::Tuple(members) => {
|
||||
let mut tys = Vec::with_capacity(members.len());
|
||||
let mut hir_members = Vec::with_capacity(members.len());
|
||||
for pat in members {
|
||||
let ty = self.fresh_ex();
|
||||
hir_members.push(self.bind_pattern(pat, ty.clone())?);
|
||||
tys.push(ty);
|
||||
}
|
||||
let tuple_type = Type::Tuple(tys);
|
||||
self.unify(&tuple_type, &type_)?;
|
||||
Ok(hir::Pattern::Tuple(hir_members))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tc_expr(&mut self, expr: ast::Expr<'ast>) -> Result<hir::Expr<'ast, Type>> {
|
||||
match expr {
|
||||
ast::Expr::Ident(ident) => {
|
||||
let type_ = self
|
||||
.env
|
||||
.resolve(&ident)
|
||||
.ok_or_else(|| Error::UndefinedVariable(ident.to_owned()))?
|
||||
.clone();
|
||||
Ok(hir::Expr::Ident(ident, type_))
|
||||
}
|
||||
ast::Expr::Literal(lit) => {
|
||||
let type_ = match lit {
|
||||
Literal::Int(_) => Type::Prim(PrimType::Int),
|
||||
Literal::Bool(_) => Type::Prim(PrimType::Bool),
|
||||
Literal::String(_) => Type::Prim(PrimType::CString),
|
||||
Literal::Unit => Type::Unit,
|
||||
};
|
||||
Ok(hir::Expr::Literal(lit.to_owned(), type_))
|
||||
}
|
||||
ast::Expr::Tuple(members) => {
|
||||
let members = members
|
||||
.into_iter()
|
||||
.map(|expr| self.tc_expr(expr))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let type_ = Type::Tuple(members.iter().map(|expr| expr.type_().clone()).collect());
|
||||
Ok(hir::Expr::Tuple(members, type_))
|
||||
}
|
||||
ast::Expr::UnaryOp { op, rhs } => todo!(),
|
||||
ast::Expr::BinaryOp { lhs, op, rhs } => {
|
||||
let lhs = self.tc_expr(*lhs)?;
|
||||
let rhs = self.tc_expr(*rhs)?;
|
||||
let type_ = match op {
|
||||
BinaryOperator::Equ | BinaryOperator::Neq => {
|
||||
self.unify(lhs.type_(), rhs.type_())?;
|
||||
Type::Prim(PrimType::Bool)
|
||||
}
|
||||
BinaryOperator::Add | BinaryOperator::Sub | BinaryOperator::Mul => {
|
||||
let ty = self.unify(lhs.type_(), rhs.type_())?;
|
||||
// if !matches!(ty, Type::Int | Type::Float) {
|
||||
// return Err(Error::NonNumeric(ty));
|
||||
// }
|
||||
ty
|
||||
}
|
||||
BinaryOperator::Div => todo!(),
|
||||
BinaryOperator::Pow => todo!(),
|
||||
};
|
||||
Ok(hir::Expr::BinaryOp {
|
||||
lhs: Box::new(lhs),
|
||||
op,
|
||||
rhs: Box::new(rhs),
|
||||
type_,
|
||||
})
|
||||
}
|
||||
ast::Expr::Let { bindings, body } => {
|
||||
self.env.push();
|
||||
let bindings = bindings
|
||||
.into_iter()
|
||||
.map(
|
||||
|ast::Binding { pat, type_, body }| -> Result<hir::Binding<Type>> {
|
||||
let body = self.tc_expr(body)?;
|
||||
if let Some(type_) = type_ {
|
||||
let type_ = self.type_from_ast_type(type_);
|
||||
self.unify(body.type_(), &type_)?;
|
||||
}
|
||||
let pat = self.bind_pattern(pat, body.type_().clone())?;
|
||||
Ok(hir::Binding { pat, body })
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<hir::Binding<Type>>>>()?;
|
||||
let body = self.tc_expr(*body)?;
|
||||
self.env.pop();
|
||||
Ok(hir::Expr::Let {
|
||||
bindings,
|
||||
type_: body.type_().clone(),
|
||||
body: Box::new(body),
|
||||
})
|
||||
}
|
||||
ast::Expr::If {
|
||||
condition,
|
||||
then,
|
||||
else_,
|
||||
} => {
|
||||
let condition = self.tc_expr(*condition)?;
|
||||
self.unify(&Type::Prim(PrimType::Bool), condition.type_())?;
|
||||
let then = self.tc_expr(*then)?;
|
||||
let else_ = self.tc_expr(*else_)?;
|
||||
let type_ = self.unify(then.type_(), else_.type_())?;
|
||||
Ok(hir::Expr::If {
|
||||
condition: Box::new(condition),
|
||||
then: Box::new(then),
|
||||
else_: Box::new(else_),
|
||||
type_,
|
||||
})
|
||||
}
|
||||
ast::Expr::Fun(f) => {
|
||||
let ast::Fun { args, body } = *f;
|
||||
self.env.push();
|
||||
let args: Vec<_> = args
|
||||
.into_iter()
|
||||
.map(|Arg { ident, type_ }| {
|
||||
let ty = match type_ {
|
||||
Some(t) => self.type_from_ast_type(t),
|
||||
None => self.fresh_ex(),
|
||||
};
|
||||
self.env.set(ident.clone(), ty.clone());
|
||||
(ident, ty)
|
||||
})
|
||||
.collect();
|
||||
let body = self.tc_expr(body)?;
|
||||
self.env.pop();
|
||||
Ok(hir::Expr::Fun {
|
||||
type_: Type::Fun {
|
||||
args: args.iter().map(|(_, ty)| ty.clone()).collect(),
|
||||
ret: Box::new(body.type_().clone()),
|
||||
},
|
||||
type_args: vec![], // TODO fill in once we do let generalization
|
||||
args,
|
||||
body: Box::new(body),
|
||||
})
|
||||
}
|
||||
ast::Expr::Call { fun, args } => {
|
||||
let ret_ty = self.fresh_ex();
|
||||
let arg_tys = args.iter().map(|_| self.fresh_ex()).collect::<Vec<_>>();
|
||||
let ft = Type::Fun {
|
||||
args: arg_tys.clone(),
|
||||
ret: Box::new(ret_ty.clone()),
|
||||
};
|
||||
let fun = self.tc_expr(*fun)?;
|
||||
self.instantiations.push();
|
||||
self.unify(&ft, fun.type_())?;
|
||||
let args = args
|
||||
.into_iter()
|
||||
.zip(arg_tys)
|
||||
.map(|(arg, ty)| {
|
||||
let arg = self.tc_expr(arg)?;
|
||||
self.unify(&ty, arg.type_())?;
|
||||
Ok(arg)
|
||||
})
|
||||
.try_collect()?;
|
||||
let type_args = self.commit_instantiations();
|
||||
Ok(hir::Expr::Call {
|
||||
fun: Box::new(fun),
|
||||
type_args,
|
||||
args,
|
||||
type_: ret_ty,
|
||||
})
|
||||
}
|
||||
ast::Expr::Ascription { expr, type_ } => {
|
||||
let expr = self.tc_expr(*expr)?;
|
||||
let type_ = self.type_from_ast_type(type_);
|
||||
self.unify(expr.type_(), &type_)?;
|
||||
Ok(expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tc_decl(
|
||||
&mut self,
|
||||
decl: ast::Decl<'ast>,
|
||||
) -> Result<Option<hir::Decl<'ast, Type>>> {
|
||||
match decl {
|
||||
ast::Decl::Fun { name, body } => {
|
||||
let mut expr = ast::Expr::Fun(Box::new(body));
|
||||
if let Some(type_) = self.env.resolve(&name) {
|
||||
expr = ast::Expr::Ascription {
|
||||
expr: Box::new(expr),
|
||||
type_: self.finalize_type(type_.clone())?,
|
||||
};
|
||||
}
|
||||
|
||||
self.env.push();
|
||||
let body = self.tc_expr(expr)?;
|
||||
let type_ = body.type_().clone();
|
||||
self.env.set(name.clone(), type_);
|
||||
self.env.pop();
|
||||
match body {
|
||||
hir::Expr::Fun {
|
||||
type_args,
|
||||
args,
|
||||
body,
|
||||
type_,
|
||||
} => Ok(Some(hir::Decl::Fun {
|
||||
name,
|
||||
type_args,
|
||||
args,
|
||||
body,
|
||||
type_,
|
||||
})),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
ast::Decl::Ascription { name, type_ } => {
|
||||
let type_ = self.type_from_ast_type(type_);
|
||||
self.env.set(name.clone(), type_);
|
||||
Ok(None)
|
||||
}
|
||||
ast::Decl::Extern { name, type_ } => {
|
||||
let type_ = self.type_from_ast_type(ast::Type::Function(type_));
|
||||
self.env.set(name.clone(), type_.clone());
|
||||
let (arg_types, ret_type) = match type_ {
|
||||
Type::Fun { args, ret } => (args, *ret),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Ok(Some(hir::Decl::Extern {
|
||||
name,
|
||||
arg_types,
|
||||
ret_type,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fresh_tv(&mut self) -> TyVar {
|
||||
self.ty_var_namer.make_name()
|
||||
}
|
||||
|
||||
fn fresh_ex(&mut self) -> Type {
|
||||
Type::Exist(self.fresh_tv())
|
||||
}
|
||||
|
||||
fn fresh_univ(&mut self) -> Type {
|
||||
Type::Univ(self.fresh_tv())
|
||||
}
|
||||
|
||||
fn unify(&mut self, ty1: &Type, ty2: &Type) -> Result<Type> {
|
||||
match (ty1, ty2) {
|
||||
(Type::Unit, Type::Unit) => Ok(Type::Unit),
|
||||
(Type::Exist(tv), ty) | (ty, Type::Exist(tv)) => match self.resolve_tv(*tv)? {
|
||||
Some(existing_ty) if self.types_match(ty, &existing_ty) => Ok(ty.clone()),
|
||||
Some(var @ ast::Type::Var(_)) => {
|
||||
let var = self.type_from_ast_type(var);
|
||||
self.unify(&var, ty)
|
||||
}
|
||||
Some(existing_ty) => match ty {
|
||||
Type::Exist(_) => {
|
||||
let rhs = self.type_from_ast_type(existing_ty);
|
||||
self.unify(ty, &rhs)
|
||||
}
|
||||
_ => Err(Error::TypeMismatch {
|
||||
expected: ty.clone(),
|
||||
actual: self.type_from_ast_type(existing_ty),
|
||||
}),
|
||||
},
|
||||
None => match self.ctx.insert(*tv, ty.clone()) {
|
||||
Some(existing) => self.unify(&existing, ty),
|
||||
None => Ok(ty.clone()),
|
||||
},
|
||||
},
|
||||
(Type::Univ(u1), Type::Univ(u2)) if u1 == u2 => Ok(ty2.clone()),
|
||||
(Type::Univ(u), ty) | (ty, Type::Univ(u)) => {
|
||||
let ident = self.name_univ(*u);
|
||||
match self.instantiations.resolve(&ident) {
|
||||
Some(existing_ty) if ty == existing_ty => Ok(ty.clone()),
|
||||
Some(existing_ty) => Err(Error::TypeMismatch {
|
||||
expected: ty.clone(),
|
||||
actual: existing_ty.clone(),
|
||||
}),
|
||||
None => {
|
||||
self.instantiations.set(ident, ty.clone());
|
||||
Ok(ty.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
(Type::Prim(p1), Type::Prim(p2)) if p1 == p2 => Ok(ty2.clone()),
|
||||
(Type::Tuple(t1), Type::Tuple(t2)) if t1.len() == t2.len() => {
|
||||
let ts = t1
|
||||
.iter()
|
||||
.zip(t2.iter())
|
||||
.map(|(t1, t2)| self.unify(t1, t2))
|
||||
.try_collect()?;
|
||||
Ok(Type::Tuple(ts))
|
||||
}
|
||||
(
|
||||
Type::Fun {
|
||||
args: args1,
|
||||
ret: ret1,
|
||||
},
|
||||
Type::Fun {
|
||||
args: args2,
|
||||
ret: ret2,
|
||||
},
|
||||
) => {
|
||||
let args = args1
|
||||
.iter()
|
||||
.zip(args2)
|
||||
.map(|(t1, t2)| self.unify(t1, t2))
|
||||
.try_collect()?;
|
||||
let ret = self.unify(ret1, ret2)?;
|
||||
Ok(Type::Fun {
|
||||
args,
|
||||
ret: Box::new(ret),
|
||||
})
|
||||
}
|
||||
(Type::Nullary(_), _) | (_, Type::Nullary(_)) => todo!(),
|
||||
_ => Err(Error::TypeMismatch {
|
||||
expected: ty1.clone(),
|
||||
actual: ty2.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_expr(
|
||||
&self,
|
||||
expr: hir::Expr<'ast, Type>,
|
||||
) -> Result<hir::Expr<'ast, ast::Type<'ast>>> {
|
||||
expr.traverse_type(|ty| self.finalize_type(ty))
|
||||
}
|
||||
|
||||
fn finalize_decl(
|
||||
&mut self,
|
||||
decl: hir::Decl<'ast, Type>,
|
||||
) -> Result<hir::Decl<'ast, ast::Type<'ast>>> {
|
||||
let res = decl.traverse_type(|ty| self.finalize_type(ty))?;
|
||||
if let Some(type_) = res.type_() {
|
||||
let ty = self.type_from_ast_type(type_.clone());
|
||||
self.env.set(res.name().clone(), ty);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn finalize_type(&self, ty: Type) -> Result<ast::Type<'static>> {
|
||||
let ret = match ty {
|
||||
Type::Exist(tv) => self.resolve_tv(tv)?.ok_or(Error::AmbiguousType(tv)),
|
||||
Type::Univ(tv) => Ok(ast::Type::Var(self.name_univ(tv))),
|
||||
Type::Unit => Ok(ast::Type::Unit),
|
||||
Type::Nullary(_) => todo!(),
|
||||
Type::Prim(pr) => Ok(pr.into()),
|
||||
Type::Tuple(members) => Ok(ast::Type::Tuple(
|
||||
members
|
||||
.into_iter()
|
||||
.map(|ty| self.finalize_type(ty))
|
||||
.try_collect()?,
|
||||
)),
|
||||
Type::Fun { args, ret } => Ok(ast::Type::Function(ast::FunctionType {
|
||||
args: args
|
||||
.into_iter()
|
||||
.map(|ty| self.finalize_type(ty))
|
||||
.try_collect()?,
|
||||
ret: Box::new(self.finalize_type(*ret)?),
|
||||
})),
|
||||
};
|
||||
ret
|
||||
}
|
||||
|
||||
fn resolve_tv(&self, tv: TyVar) -> Result<Option<ast::Type<'static>>> {
|
||||
let mut res = &Type::Exist(tv);
|
||||
Ok(loop {
|
||||
match res {
|
||||
Type::Exist(tv) => {
|
||||
res = match self.ctx.get(tv) {
|
||||
Some(r) => r,
|
||||
None => return Ok(None),
|
||||
};
|
||||
}
|
||||
Type::Univ(tv) => {
|
||||
let ident = self.name_univ(*tv);
|
||||
if let Some(r) = self.instantiations.resolve(&ident) {
|
||||
res = r;
|
||||
} else {
|
||||
break Some(ast::Type::Var(ident));
|
||||
}
|
||||
}
|
||||
Type::Nullary(_) => todo!(),
|
||||
Type::Prim(pr) => break Some((*pr).into()),
|
||||
Type::Unit => break Some(ast::Type::Unit),
|
||||
Type::Fun { args, ret } => todo!(),
|
||||
Type::Tuple(_) => break Some(self.finalize_type(res.clone())?),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn type_from_ast_type(&mut self, ast_type: ast::Type<'ast>) -> Type {
|
||||
match ast_type {
|
||||
ast::Type::Unit => Type::Unit,
|
||||
ast::Type::Int => INT,
|
||||
ast::Type::Float => FLOAT,
|
||||
ast::Type::Bool => BOOL,
|
||||
ast::Type::CString => CSTRING,
|
||||
ast::Type::Tuple(members) => Type::Tuple(
|
||||
members
|
||||
.into_iter()
|
||||
.map(|ty| self.type_from_ast_type(ty))
|
||||
.collect(),
|
||||
),
|
||||
ast::Type::Function(ast::FunctionType { args, ret }) => Type::Fun {
|
||||
args: args
|
||||
.into_iter()
|
||||
.map(|t| self.type_from_ast_type(t))
|
||||
.collect(),
|
||||
ret: Box::new(self.type_from_ast_type(*ret)),
|
||||
},
|
||||
ast::Type::Var(id) => Type::Univ({
|
||||
let opt_tv = { self.type_vars.borrow_mut().0.get_by_left(&id).copied() };
|
||||
opt_tv.unwrap_or_else(|| {
|
||||
let tv = self.fresh_tv();
|
||||
self.type_vars
|
||||
.borrow_mut()
|
||||
.0
|
||||
.insert_no_overwrite(id, tv)
|
||||
.unwrap();
|
||||
tv
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn name_univ(&self, tv: TyVar) -> Ident<'static> {
|
||||
let mut vars = self.type_vars.borrow_mut();
|
||||
vars.0
|
||||
.get_by_right(&tv)
|
||||
.map(Ident::to_owned)
|
||||
.unwrap_or_else(|| {
|
||||
let name = loop {
|
||||
let name = vars.1.make_name();
|
||||
if !vars.0.contains_left(&name) {
|
||||
break name;
|
||||
}
|
||||
};
|
||||
vars.0.insert_no_overwrite(name.clone(), tv).unwrap();
|
||||
name
|
||||
})
|
||||
}
|
||||
|
||||
fn commit_instantiations(&mut self) -> HashMap<Ident<'ast>, Type> {
|
||||
let mut res = HashMap::new();
|
||||
let mut ctx = mem::take(&mut self.ctx);
|
||||
for (_, v) in ctx.iter_mut() {
|
||||
if let Type::Univ(tv) = v {
|
||||
let tv_name = self.name_univ(*tv);
|
||||
if let Some(concrete) = self.instantiations.resolve(&tv_name) {
|
||||
res.insert(tv_name, concrete.clone());
|
||||
*v = concrete.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.ctx = ctx;
|
||||
self.instantiations.pop();
|
||||
res
|
||||
}
|
||||
|
||||
fn types_match(&self, type_: &Type, ast_type: &ast::Type<'ast>) -> bool {
|
||||
match (type_, ast_type) {
|
||||
(Type::Univ(u), ast::Type::Var(v)) => {
|
||||
Some(u) == self.type_vars.borrow().0.get_by_left(v)
|
||||
}
|
||||
(Type::Univ(_), _) => false,
|
||||
(Type::Exist(_), _) => false,
|
||||
(Type::Unit, ast::Type::Unit) => true,
|
||||
(Type::Unit, _) => false,
|
||||
(Type::Nullary(_), _) => todo!(),
|
||||
(Type::Prim(pr), ty) => ast::Type::from(*pr) == *ty,
|
||||
(Type::Tuple(members), ast::Type::Tuple(members2)) => members
|
||||
.iter()
|
||||
.zip(members2.iter())
|
||||
.all(|(t1, t2)| self.types_match(t1, t2)),
|
||||
(Type::Tuple(members), _) => false,
|
||||
(Type::Fun { args, ret }, ast::Type::Function(ft)) => {
|
||||
args.len() == ft.args.len()
|
||||
&& args
|
||||
.iter()
|
||||
.zip(&ft.args)
|
||||
.all(|(a1, a2)| self.types_match(a1, &a2))
|
||||
&& self.types_match(&*ret, &*ft.ret)
|
||||
}
|
||||
(Type::Fun { .. }, _) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typecheck_expr(expr: ast::Expr) -> Result<hir::Expr<ast::Type>> {
|
||||
let mut typechecker = Typechecker::new();
|
||||
let typechecked = typechecker.tc_expr(expr)?;
|
||||
typechecker.finalize_expr(typechecked)
|
||||
}
|
||||
|
||||
pub fn typecheck_toplevel(decls: Vec<ast::Decl>) -> Result<Vec<hir::Decl<ast::Type>>> {
|
||||
let mut typechecker = Typechecker::new();
|
||||
let mut res = Vec::with_capacity(decls.len());
|
||||
for decl in decls {
|
||||
if let Some(hir_decl) = typechecker.tc_decl(decl)? {
|
||||
let hir_decl = typechecker.finalize_decl(hir_decl)?;
|
||||
res.push(hir_decl);
|
||||
}
|
||||
typechecker.ctx.clear();
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! assert_type {
|
||||
($expr: expr, $type: expr) => {
|
||||
use crate::parser::{expr, type_};
|
||||
let parsed_expr = test_parse!(expr, $expr);
|
||||
let parsed_type = test_parse!(type_, $type);
|
||||
let res = typecheck_expr(parsed_expr).unwrap_or_else(|e| panic!("{}", e));
|
||||
assert!(
|
||||
res.type_().alpha_equiv(&parsed_type),
|
||||
"{} inferred type {}, but expected {}",
|
||||
$expr,
|
||||
res.type_(),
|
||||
$type
|
||||
);
|
||||
};
|
||||
|
||||
(toplevel($program: expr), $($decl: ident => $type: expr),+ $(,)?) => {{
|
||||
use crate::parser::{toplevel, type_};
|
||||
let program = test_parse!(toplevel, $program);
|
||||
let res = typecheck_toplevel(program).unwrap_or_else(|e| panic!("{}", e));
|
||||
$(
|
||||
let parsed_type = test_parse!(type_, $type);
|
||||
let ident = Ident::try_from(::std::stringify!($decl)).unwrap();
|
||||
let decl = res.iter().find(|decl| {
|
||||
matches!(decl, crate::ast::hir::Decl::Fun { name, .. } if name == &ident)
|
||||
}).unwrap_or_else(|| panic!("Could not find declaration for {}", ident));
|
||||
assert!(
|
||||
decl.type_().unwrap().alpha_equiv(&parsed_type),
|
||||
"inferred type {} for {}, but expected {}",
|
||||
decl.type_().unwrap(),
|
||||
ident,
|
||||
$type
|
||||
);
|
||||
)+
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_type_error {
|
||||
($expr: expr) => {
|
||||
use crate::parser::expr;
|
||||
let parsed_expr = test_parse!(expr, $expr);
|
||||
let res = typecheck_expr(parsed_expr);
|
||||
assert!(
|
||||
res.is_err(),
|
||||
"Expected type error, but got type: {}",
|
||||
res.unwrap().type_()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_int() {
|
||||
assert_type!("1", "int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional() {
|
||||
assert_type!("if 1 == 2 then 3 else 4", "int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn add_bools() {
|
||||
assert_type_error!("true + false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_generic_function() {
|
||||
assert_type!("(fn x = x) 1", "int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_let_bound_generic() {
|
||||
assert_type!("let id = fn x = x in id 1", "int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn universal_ascripted_let() {
|
||||
assert_type!("let id: fn a -> a = fn x = x in id 1", "int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_generic_function_toplevel() {
|
||||
assert_type!(
|
||||
toplevel(
|
||||
"ty id : fn a -> a
|
||||
fn id x = x
|
||||
|
||||
fn main = id 0"
|
||||
),
|
||||
main => "fn -> int",
|
||||
id => "fn a -> a",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn let_generalization() {
|
||||
assert_type!("let id = fn x = x in if id true then id 1 else 2", "int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concrete_function() {
|
||||
assert_type!("fn x = x + 1", "fn int -> int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arg_ascriptions() {
|
||||
assert_type!("fn (x: int) = x", "fn int -> int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_concrete_function() {
|
||||
assert_type!("(fn x = x + 1) 2", "int");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_non_bool() {
|
||||
assert_type_error!("if 3 then true else false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn let_int() {
|
||||
assert_type!("let x = 1 in x", "int");
|
||||
}
|
||||
}
|
||||
79
users/aspen/achilles/tests/compile.rs
Normal file
79
users/aspen/achilles/tests/compile.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
use std::process::Command;
|
||||
|
||||
use crate_root::root;
|
||||
|
||||
struct Fixture {
|
||||
name: &'static str,
|
||||
exit_code: i32,
|
||||
expected_output: &'static str,
|
||||
}
|
||||
|
||||
const FIXTURES: &[Fixture] = &[
|
||||
Fixture {
|
||||
name: "simple",
|
||||
exit_code: 5,
|
||||
expected_output: "",
|
||||
},
|
||||
Fixture {
|
||||
name: "functions",
|
||||
exit_code: 9,
|
||||
expected_output: "",
|
||||
},
|
||||
Fixture {
|
||||
name: "externs",
|
||||
exit_code: 0,
|
||||
expected_output: "foobar\n",
|
||||
},
|
||||
Fixture {
|
||||
name: "units",
|
||||
exit_code: 0,
|
||||
expected_output: "hi\n",
|
||||
},
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn compile_and_run_files() {
|
||||
let ach = root().unwrap().join("ach");
|
||||
|
||||
println!("Running: `make clean`");
|
||||
assert!(
|
||||
Command::new("make")
|
||||
.arg("clean")
|
||||
.current_dir(&ach)
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap()
|
||||
.success(),
|
||||
"make clean failed"
|
||||
);
|
||||
|
||||
for Fixture {
|
||||
name,
|
||||
exit_code,
|
||||
expected_output,
|
||||
} in FIXTURES
|
||||
{
|
||||
println!(">>> Testing: {}", name);
|
||||
|
||||
println!(" Running: `make {}`", name);
|
||||
assert!(
|
||||
Command::new("make")
|
||||
.arg(name)
|
||||
.current_dir(&ach)
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap()
|
||||
.success(),
|
||||
"make failed"
|
||||
);
|
||||
|
||||
let out_path = ach.join(name);
|
||||
println!(" Running: `{}`", out_path.to_str().unwrap());
|
||||
let output = Command::new(out_path).output().unwrap();
|
||||
assert_eq!(output.status.code().unwrap(), *exit_code,);
|
||||
assert_eq!(output.stdout, expected_output.as_bytes());
|
||||
println!(" OK");
|
||||
}
|
||||
}
|
||||
1
users/aspen/bbbg/.clj-kondo/config.edn
Normal file
1
users/aspen/bbbg/.clj-kondo/config.edn
Normal file
|
|
@ -0,0 +1 @@
|
|||
{:lint-as {garden.def/defstyles clojure.core/def}}
|
||||
1
users/aspen/bbbg/.envrc
Normal file
1
users/aspen/bbbg/.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
eval "$(lorri direnv)"
|
||||
9
users/aspen/bbbg/.gitignore
vendored
Normal file
9
users/aspen/bbbg/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/target
|
||||
/classes
|
||||
*.jar
|
||||
*.class
|
||||
/.nrepl-port
|
||||
/.cpcache
|
||||
/.clojure
|
||||
/result
|
||||
/.clj-kondo/.cache
|
||||
2
users/aspen/bbbg/Makefile
Normal file
2
users/aspen/bbbg/Makefile
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
deps.nix: deps.edn
|
||||
clj2nix ./deps.edn ./deps.nix '-A:uberjar' '-A:clj-test'
|
||||
129
users/aspen/bbbg/README.md
Normal file
129
users/aspen/bbbg/README.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Brooklyn-Based Board Gaming signup sheet
|
||||
|
||||
This directory contains a small web application that acts as a signup
|
||||
sheet and attendee tracking system for [my local board gaming
|
||||
meetup](https://www.meetup.com/brooklyn-based-board-gaming/).
|
||||
|
||||
## Development
|
||||
|
||||
### Installing dependencies
|
||||
|
||||
#### With Nix + Docker ("blessed way")
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- [Nix](https://nixos.org/)
|
||||
- [lorri](https://github.com/nix-community/lorri)
|
||||
- [Docker](https://www.docker.com/)
|
||||
|
||||
From this directory in a full checkout of depot, run the following
|
||||
commands to install all development dependencies:
|
||||
|
||||
``` shell-session
|
||||
$ pwd
|
||||
/path/to/depot/users/aspen/bbbg
|
||||
$ direnv allow
|
||||
$ lorri watch --once # Wait for a single nix shell build
|
||||
```
|
||||
|
||||
Then, to run a docker container with the development database:
|
||||
|
||||
``` shell-session
|
||||
$ pwd
|
||||
/path/to/depot/users/aspen/bbbg
|
||||
$ arion up -d
|
||||
```
|
||||
|
||||
#### Choose-your-own-adventure
|
||||
|
||||
Note that the **authoritative** source for dev dependencies is the `shell.nix`
|
||||
file in this directory - those may diverge from what's written here; if so
|
||||
follow those versions rather than these.
|
||||
|
||||
- Install the [clojure command-line
|
||||
tools](https://clojure.org/guides/getting_started), with openjdk 11
|
||||
- Install and run a postgresql 12 database, with:
|
||||
- A user with superuser priveleges, the username `bbbg` and the
|
||||
password `password`
|
||||
- A database called `bbbg` owned by that user.
|
||||
- Export the following environment variables in a context visible by
|
||||
whatever method you use to run the application:
|
||||
- `PGHOST=localhost`
|
||||
- `PGUSER=bbbg`
|
||||
- `PGDATABASE=bbbg`
|
||||
- `PGPASSWORD=bbbg`
|
||||
|
||||
### Running the application
|
||||
|
||||
Before running the app, you'll need an oauth2 client-id and client secret for a
|
||||
Discord app. The application can either load those from a
|
||||
[pass](https://www.passwordstore.org/) password store, or read them from
|
||||
plaintext files in a directory. In either case, they should be accessible at the
|
||||
paths `bbbg/discord-client-id` and `bbbg/discord-client-secret` respectively.
|
||||
|
||||
#### From the command line
|
||||
|
||||
``` shell-session
|
||||
$ clj -A:dev
|
||||
Clojure 1.11.0-alpha3
|
||||
user=> (require 'bbbg.core)
|
||||
nil
|
||||
user=> ;; Optionally, if you're using a directory with plaintext files for the discord client ID and client secret:
|
||||
user=> (bbbg.util.dev-secrets/set-backend! [:dir "/path/to/that/directory"])
|
||||
user=> (bbbg.core/run-dev)
|
||||
##<SystemMap>
|
||||
user=> (bbbg.db/migrate! (:db bbbg.core/system))
|
||||
11:57:26.536 [main] INFO migratus.core - Starting migrations { }
|
||||
11:57:26.538 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... { }
|
||||
11:57:26.883 [main] INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.impossibl.postgres.jdbc.PGDirectConnection@3cae770e { }
|
||||
11:57:26.884 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. { }
|
||||
11:57:26.923 [main] INFO migratus.core - Ending migrations { }
|
||||
nil
|
||||
```
|
||||
|
||||
This will run a web server for the application listening at
|
||||
<http://localhost:8888>
|
||||
|
||||
#### In Emacs, with [CIDER](https://docs.cider.mx/cider/index.html) + [direnv](https://github.com/wbolster/emacs-direnv)
|
||||
|
||||
Open `//users/aspen/bbbg/src/bbbg/core.clj` in a buffer, then follow the
|
||||
instructions at the end of the file
|
||||
|
||||
## Deployment
|
||||
|
||||
### With nix+terraform
|
||||
|
||||
Deployment configuration is located in the `tf.nix` file, which is
|
||||
currently tightly coupled to my own infrastructure and AWS account but
|
||||
could hypothetically be adjusted to be general-purpose.
|
||||
|
||||
To deploy a new version of the application, after following "installing
|
||||
dependencies" above, run the following command in a context with ec2
|
||||
credentials available:
|
||||
|
||||
``` shell-session
|
||||
$ terraform apply
|
||||
```
|
||||
|
||||
The current deploy configuration includes:
|
||||
|
||||
- An ec2 instance running nixos, with a postgresql database and the
|
||||
bbbg application running as a service, behind nginx with an
|
||||
auto-renewing letsencrypt cert
|
||||
- The DNS A record for `bbbg.gws.fyi` pointing at that ec2 instance,
|
||||
in the cloudflare zone for `gws.fyi`
|
||||
|
||||
### Otherwise
|
||||
|
||||
¯\\\_(ツ)_/¯
|
||||
|
||||
You'll need:
|
||||
|
||||
- An uberjar for bbbg; the canonical way of building that is `nix-build
|
||||
/path/to/depot -A users.aspen.bbbg.server-jar` but I\'m not sure how that
|
||||
works outside of nix
|
||||
- A postgresql database
|
||||
- Environment variables telling the app how to connect to that
|
||||
database. See `config.systemd.services.bbbg-server.environment` in
|
||||
`module.nix` for which env vars are currently being exported by the
|
||||
NixOS module that runs the production version of the app
|
||||
15
users/aspen/bbbg/arion-compose.nix
Normal file
15
users/aspen/bbbg/arion-compose.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{ ... }:
|
||||
|
||||
{
|
||||
services = {
|
||||
postgres.service = {
|
||||
image = "postgres:12";
|
||||
environment = {
|
||||
POSTGRES_DB = "bbbg";
|
||||
POSTGRES_USER = "bbbg";
|
||||
POSTGRES_PASSWORD = "password";
|
||||
};
|
||||
ports = [ "5432:5432" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
2
users/aspen/bbbg/arion-pkgs.nix
Normal file
2
users/aspen/bbbg/arion-pkgs.nix
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
let depot = import ../../.. { };
|
||||
in depot.third_party.nixpkgs
|
||||
82
users/aspen/bbbg/default.nix
Normal file
82
users/aspen/bbbg/default.nix
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
args@{ depot, pkgs, ... }:
|
||||
|
||||
with pkgs.lib;
|
||||
|
||||
let
|
||||
inherit (depot.third_party) gitignoreSource;
|
||||
|
||||
deps = import ./deps.nix {
|
||||
inherit (pkgs) fetchMavenArtifact fetchgit lib;
|
||||
};
|
||||
in
|
||||
rec {
|
||||
meta.ci.targets = [
|
||||
"db-util"
|
||||
"server"
|
||||
"tf"
|
||||
];
|
||||
|
||||
depsPaths = deps.makePaths { };
|
||||
|
||||
resources = builtins.filterSource (_: type: type != "symlink") ./resources;
|
||||
|
||||
classpath.dev = concatStringsSep ":" (
|
||||
(map gitignoreSource [ ./src ./test ./env/dev ]) ++ [ resources ] ++ depsPaths
|
||||
);
|
||||
|
||||
classpath.test = concatStringsSep ":" (
|
||||
(map gitignoreSource [ ./src ./test ./env/test ]) ++ [ resources ] ++ depsPaths
|
||||
);
|
||||
|
||||
classpath.prod = concatStringsSep ":" (
|
||||
(map gitignoreSource [ ./src ./env/prod ]) ++ [ resources ] ++ depsPaths
|
||||
);
|
||||
|
||||
testClojure = pkgs.writeShellScript "test-clojure" ''
|
||||
export HOME=$(pwd)
|
||||
${pkgs.clojure}/bin/clojure -Scp ${depsPaths}
|
||||
'';
|
||||
|
||||
mkJar = name: opts:
|
||||
with pkgs;
|
||||
assert (hasSuffix ".jar" name);
|
||||
stdenv.mkDerivation rec {
|
||||
inherit name;
|
||||
dontUnpack = true;
|
||||
buildPhase = ''
|
||||
export HOME=$(pwd)
|
||||
cp ${./pom.xml} pom.xml
|
||||
cp ${./deps.edn} deps.edn
|
||||
${clojure}/bin/clojure \
|
||||
-Scp ${classpath.prod} \
|
||||
-A:uberjar \
|
||||
${name} \
|
||||
-C ${opts}
|
||||
'';
|
||||
|
||||
doCheck = true;
|
||||
|
||||
checkPhase = ''
|
||||
echo "checking for existence of ${name}"
|
||||
[ -f ${name} ]
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
cp ${name} $out
|
||||
'';
|
||||
};
|
||||
|
||||
db-util-jar = mkJar "bbbg-db-util.jar" "-m bbbg.db";
|
||||
|
||||
db-util = pkgs.writeShellScriptBin "bbbg-db-util" ''
|
||||
exec ${pkgs.openjdk17_headless}/bin/java -jar ${db-util-jar} "$@"
|
||||
'';
|
||||
|
||||
server-jar = mkJar "bbbg-server.jar" "-m bbbg.core";
|
||||
|
||||
server = pkgs.writeShellScriptBin "bbbg-server" ''
|
||||
exec ${pkgs.openjdk17_headless}/bin/java -jar ${server-jar} "$@"
|
||||
'';
|
||||
|
||||
tf = import ./tf.nix args;
|
||||
}
|
||||
70
users/aspen/bbbg/deps.edn
Normal file
70
users/aspen/bbbg/deps.edn
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{:deps
|
||||
{org.clojure/clojure {:mvn/version "1.11.0-alpha3"}
|
||||
|
||||
;; DB
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.2.761"}
|
||||
com.impossibl.pgjdbc-ng/pgjdbc-ng {:mvn/version "0.8.9"}
|
||||
com.zaxxer/HikariCP {:mvn/version "5.0.0"}
|
||||
migratus/migratus {:mvn/version "1.3.5"}
|
||||
com.github.seancorfield/honeysql {:mvn/version "2.2.840"}
|
||||
nilenso/honeysql-postgres {:mvn/version "0.4.112"}
|
||||
|
||||
;; HTTP
|
||||
http-kit/http-kit {:mvn/version "2.5.3"}
|
||||
ring/ring {:mvn/version "1.9.4"}
|
||||
compojure/compojure {:mvn/version "1.6.2"}
|
||||
javax.servlet/servlet-api {:mvn/version "2.5"}
|
||||
ring-oauth2/ring-oauth2 {:mvn/version "0.2.0"}
|
||||
clj-http/clj-http {:mvn/version "3.12.3"}
|
||||
ring-logger/ring-logger {:mvn/version "1.0.1"}
|
||||
|
||||
;; Web
|
||||
hiccup/hiccup {:mvn/version "1.0.5"}
|
||||
garden/garden {:mvn/version "1.3.10"}
|
||||
|
||||
|
||||
;; Logging + Observability
|
||||
ch.qos.logback/logback-classic {:mvn/version "1.3.0-alpha12"}
|
||||
org.slf4j/jul-to-slf4j {:mvn/version "2.0.0-alpha4"}
|
||||
org.slf4j/jcl-over-slf4j {:mvn/version "2.0.0-alpha4"}
|
||||
org.slf4j/log4j-over-slf4j {:mvn/version "2.0.0-alpha4"}
|
||||
cambium/cambium.core {:mvn/version "1.1.1"}
|
||||
cambium/cambium.codec-cheshire {:mvn/version "1.0.0"}
|
||||
cambium/cambium.logback.core {:mvn/version "0.4.5"}
|
||||
cambium/cambium.logback.json {:mvn/version "0.4.5"}
|
||||
clj-commons/iapetos {:mvn/version "0.1.12"}
|
||||
|
||||
;; Utilities
|
||||
com.stuartsierra/component {:mvn/version "1.0.0"}
|
||||
yogthos/config {:mvn/version "1.1.9"}
|
||||
clojure.java-time/clojure.java-time {:mvn/version "0.3.3"}
|
||||
cheshire/cheshire {:mvn/version "5.10.1"}
|
||||
org.apache.commons/commons-lang3 {:mvn/version "3.12.0"}
|
||||
org.clojure/data.csv {:mvn/version "1.0.0"}
|
||||
|
||||
;; Spec
|
||||
org.clojure/spec.alpha {:mvn/version "0.3.218"}
|
||||
org.clojure/core.specs.alpha {:mvn/version "0.2.62"}
|
||||
expound/expound {:mvn/version "0.8.10"}
|
||||
org.clojure/test.check {:mvn/version "1.1.1"}}
|
||||
|
||||
:paths
|
||||
["src"
|
||||
"test"
|
||||
"resources"
|
||||
"target/classes"]
|
||||
:aliases
|
||||
{:dev {:extra-paths ["env/dev"]
|
||||
:jvm-opts ["-XX:-OmitStackTraceInFastThrow"]}
|
||||
:clj-test {:extra-paths ["test" "env/test"]
|
||||
:extra-deps {io.github.cognitect-labs/test-runner
|
||||
{:git/url "https://github.com/cognitect-labs/test-runner"
|
||||
:sha "cc75980b43011773162b485f46f939dc5fba91e4"}}
|
||||
:main-opts ["-m" "cognitect.test-runner"
|
||||
"-d" "test"]}
|
||||
:uberjar {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}}
|
||||
:extra-paths ["env/prod"]
|
||||
:main-opts ["-m" "hf.depstar.uberjar"]}
|
||||
|
||||
:outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "1.3.1"}}
|
||||
:main-opts ["-m" "antq.core"]}}}
|
||||
1494
users/aspen/bbbg/deps.nix
Normal file
1494
users/aspen/bbbg/deps.nix
Normal file
File diff suppressed because it is too large
Load diff
3
users/aspen/bbbg/env/dev/bbbg-signup/env.clj
vendored
Normal file
3
users/aspen/bbbg/env/dev/bbbg-signup/env.clj
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
(ns bbbg.env)
|
||||
|
||||
(def environment :env/dev)
|
||||
15
users/aspen/bbbg/env/dev/logback.xml
vendored
Normal file
15
users/aspen/bbbg/env/dev/logback.xml
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg { %mdc }%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
<logger name="user" level="ALL" />
|
||||
<logger name="ci.windtunnel" level="ALL" />
|
||||
</configuration>
|
||||
3
users/aspen/bbbg/env/prod/bbbg-signup/env.clj
vendored
Normal file
3
users/aspen/bbbg/env/prod/bbbg-signup/env.clj
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
(ns bbbg.env)
|
||||
|
||||
(def environment :env/prod)
|
||||
31
users/aspen/bbbg/env/prod/logback.xml
vendored
Normal file
31
users/aspen/bbbg/env/prod/logback.xml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<!-- Silence Logback's own status messages about config parsing -->
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||
|
||||
<!-- Console output -->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- Only log level INFO and above -->
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||
<layout class="cambium.logback.json.FlatJsonLayout">
|
||||
<jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
|
||||
<prettyPrint>false</prettyPrint>
|
||||
</jsonFormatter>
|
||||
<!-- <context>api</context> -->
|
||||
<timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</timestampFormat>
|
||||
<timestampFormatTimezoneId>UTC</timestampFormatTimezoneId>
|
||||
<appendLineSeparator>true</appendLineSeparator>
|
||||
</layout>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
<logger name="user" level="ALL" />
|
||||
</configuration>
|
||||
3
users/aspen/bbbg/env/test/bbbg-signup/env.clj
vendored
Normal file
3
users/aspen/bbbg/env/test/bbbg-signup/env.clj
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
(ns bbbg.env)
|
||||
|
||||
(def environment :env/test)
|
||||
11
users/aspen/bbbg/env/test/logback.xml
vendored
Normal file
11
users/aspen/bbbg/env/test/logback.xml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<pattern>%msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="OFF">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
137
users/aspen/bbbg/module.nix
Normal file
137
users/aspen/bbbg/module.nix
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
{ config, lib, pkgs, depot, ... }:
|
||||
|
||||
let
|
||||
bbbg = depot.users.aspen.bbbg;
|
||||
cfg = config.services.bbbg;
|
||||
in
|
||||
{
|
||||
options = with lib; {
|
||||
services.bbbg = {
|
||||
enable = mkEnableOption "BBBG Server";
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = 7222;
|
||||
description = "Port to listen to for the HTTP server";
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
default = "bbbg.gws.fyi";
|
||||
description = "Domain to host under";
|
||||
};
|
||||
|
||||
proxy = {
|
||||
enable = mkEnableOption "NGINX reverse proxy";
|
||||
};
|
||||
|
||||
database = {
|
||||
enable = mkEnableOption "BBBG Database Server";
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "bbbg";
|
||||
description = "Database username";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "bbbg";
|
||||
description = "Database name";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = 5432;
|
||||
description = "Database host";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf cfg.enable {
|
||||
systemd.services.bbbg-server = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
Restart = "always";
|
||||
EnvironmentFile = config.age.secretsDir + "/bbbg";
|
||||
};
|
||||
|
||||
environment = {
|
||||
PGHOST = cfg.database.host;
|
||||
PGUSER = cfg.database.user;
|
||||
PGDATABASE = cfg.database.name;
|
||||
PORT = toString cfg.port;
|
||||
BASE_URL = "https://${cfg.domain}";
|
||||
};
|
||||
|
||||
script = "${bbbg.server}/bin/bbbg-server";
|
||||
};
|
||||
|
||||
systemd.services.migrate-bbbg = {
|
||||
description = "Run database migrations for BBBG";
|
||||
wantedBy = [ "bbbg-server.service" ];
|
||||
after = ([ "network.target" ]
|
||||
++ (if cfg.database.enable
|
||||
then [ "postgresql.service" ]
|
||||
else [ ]));
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
EnvironmentFile = config.age.secretsDir + "/bbbg";
|
||||
};
|
||||
|
||||
environment = {
|
||||
PGHOST = cfg.database.host;
|
||||
PGUSER = cfg.database.user;
|
||||
PGDATABASE = cfg.database.name;
|
||||
};
|
||||
|
||||
script = "${bbbg.db-util}/bin/bbbg-db-util migrate";
|
||||
};
|
||||
})
|
||||
(lib.mkIf cfg.database.enable {
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
authentication = lib.mkForce ''
|
||||
local all all trust
|
||||
host all all 127.0.0.1/32 password
|
||||
host all all ::1/128 password
|
||||
hostnossl all all 127.0.0.1/32 password
|
||||
hostnossl all all ::1/128 password
|
||||
'';
|
||||
|
||||
ensureDatabases = [
|
||||
cfg.database.name
|
||||
];
|
||||
|
||||
ensureUsers = [{
|
||||
name = cfg.database.user;
|
||||
ensurePermissions = {
|
||||
"DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";
|
||||
};
|
||||
}];
|
||||
};
|
||||
})
|
||||
(lib.mkIf cfg.proxy.enable {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."${cfg.domain}" = {
|
||||
enableACME = true;
|
||||
forceSSL = true;
|
||||
locations."/".proxyPass = "http://localhost:${toString cfg.port}";
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
42
users/aspen/bbbg/pom.xml
Normal file
42
users/aspen/bbbg/pom.xml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>fyi.gws</groupId>
|
||||
<artifactId>bbbg</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<name>fyi.gws/bbbg</name>
|
||||
<description>webhook listener for per-branch deploys</description>
|
||||
<url>https://bbbg.gws.fyi</url>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Griffin Smith</name>
|
||||
</developer>
|
||||
</developers>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.clojure</groupId>
|
||||
<artifactId>clojure</artifactId>
|
||||
<version>1.11.0-alpha3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>clojars</id>
|
||||
<url>https://repo.clojars.org/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>sonatype</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>clojars</id>
|
||||
<name>Clojars repository</name>
|
||||
<url>https://clojars.org/repo</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
</project>
|
||||
152
users/aspen/bbbg/resources/base.css
Normal file
152
users/aspen/bbbg/resources/base.css
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/* montserrat-italic - latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local("Montserrat Italic"), local("Montserrat-Italic"),
|
||||
url("/fonts/montserrat-v15-latin-italic.woff2") format("woff2"),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url("/fonts/montserrat-v15-latin-italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* montserrat-regular - latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Montserrat Regular"), local("Montserrat-Regular"),
|
||||
url("/fonts/montserrat-v15-latin-regular.woff2") format("woff2"),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url("/fonts/montserrat-v15-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* montserrat-500 - latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local("Montserrat Medium"), local("Montserrat-Medium"),
|
||||
url("/fonts/montserrat-v15-latin-500.woff2") format("woff2"),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url("/fonts/montserrat-v15-latin-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* montserrat-500italic - latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
src: local("Montserrat Medium Italic"), local("Montserrat-MediumItalic"),
|
||||
url("/fonts/montserrat-v15-latin-500italic.woff2") format("woff2"),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url("/fonts/montserrat-v15-latin-500italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* montserrat-600 - latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local("Montserrat SemiBold"), local("Montserrat-SemiBold"),
|
||||
url("/fonts/montserrat-v15-latin-600.woff2") format("woff2"),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url("/fonts/montserrat-v15-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* montserrat-800 - latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local("Montserrat ExtraBold"), local("Montserrat-ExtraBold"),
|
||||
url("/fonts/montserrat-v15-latin-800.woff2") format("woff2"),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url("/fonts/montserrat-v15-latin-800.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* montserrat-800italic - latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local("Montserrat ExtraBold Italic"), local("Montserrat-ExtraBoldItalic"),
|
||||
url("/fonts/montserrat-v15-latin-800italic.woff2") format("woff2"),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url("/fonts/montserrat-v15-latin-800italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
font-family: "Montserrat", Helvetica, sans-serif;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
figure,
|
||||
figcaption,
|
||||
blockquote,
|
||||
dl,
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizeSpeed;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
ul[class],
|
||||
ol[class] {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a:not([class]) {
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
article > * + * {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
drop table "public"."user";
|
||||
|
||||
-- ;;
|
||||
|
||||
drop table "public"."event_attendee";
|
||||
|
||||
|
||||
-- ;;
|
||||
|
||||
drop table "public"."event";
|
||||
|
||||
-- ;;
|
||||
|
||||
drop table "public"."attendee";
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
-- ;;
|
||||
CREATE TABLE "attendee" (
|
||||
"id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"meetup_name" TEXT NOT NULL,
|
||||
"discord_name" TEXT,
|
||||
"meetup_user_id" TEXT,
|
||||
"organizer_notes" TEXT NOT NULL DEFAULT '',
|
||||
"created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
-- ;;
|
||||
CREATE TABLE "event" (
|
||||
"id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"date" DATE NOT NULL,
|
||||
"created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
-- ;;
|
||||
CREATE TABLE "event_attendee" (
|
||||
"event_id" UUID NOT NULL REFERENCES "event" ("id"),
|
||||
"attendee_id" UUID NOT NULL REFERENCES "attendee" ("id"),
|
||||
"rsvpd_attending" BOOL,
|
||||
"attended" BOOL,
|
||||
"created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("event_id", "attendee_id")
|
||||
);
|
||||
-- ;;
|
||||
CREATE TABLE "user" (
|
||||
"id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"username" TEXT NOT NULL,
|
||||
"discord_user_id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE "attendee_check";
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE attendee_check (
|
||||
"id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"attendee_id" UUID NOT NULL REFERENCES attendee ("id"),
|
||||
"user_id" UUID NOT NULL REFERENCES "public"."user" ("id"),
|
||||
"last_dose_at" DATE,
|
||||
"checked_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
drop index attendee_uniq_meetup_user_id;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
create unique index "attendee_uniq_meetup_user_id" on attendee (meetup_user_id);
|
||||
-- ;;
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
73
users/aspen/bbbg/resources/public/main.js
Normal file
73
users/aspen/bbbg/resources/public/main.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
window.onload = () => {
|
||||
const input = document.getElementById("name-autocomplete");
|
||||
if (input != null) {
|
||||
const attendeeList = document.getElementById("attendees-list");
|
||||
const filterAttendees = (filter) => {
|
||||
if (filter == "") {
|
||||
for (let elt of attendeeList.querySelectorAll("li")) {
|
||||
elt.classList.remove("hidden");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let re = "";
|
||||
for (let c of filter) {
|
||||
re += `${c}.*`;
|
||||
}
|
||||
let filterRe = new RegExp(re, "i");
|
||||
|
||||
for (let elt of attendeeList.querySelectorAll("li")) {
|
||||
const attendee = JSON.parse(elt.dataset.attendee);
|
||||
if (attendee["bbbg.attendee/meetup-name"].match(filterRe) == null) {
|
||||
elt.classList.add("hidden");
|
||||
} else {
|
||||
elt.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const attendeeIDInput = document.getElementById("attendee-id");
|
||||
const submit = document.querySelector("#submit-button");
|
||||
const signupForm = document.getElementById("signup-form");
|
||||
|
||||
input.oninput = (e) => {
|
||||
filterAttendees(e.target.value);
|
||||
attendeeIDInput.value = null;
|
||||
submit.classList.add("hidden");
|
||||
submit.setAttribute("disabled", "disabled");
|
||||
signupForm.setAttribute("disabled", "disabled");
|
||||
};
|
||||
|
||||
attendeeList.addEventListener("click", (e) => {
|
||||
if (!(e.target instanceof HTMLLIElement)) {
|
||||
return;
|
||||
}
|
||||
if (e.target.dataset.attendee == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attendee = JSON.parse(e.target.dataset.attendee);
|
||||
input.value = attendee["bbbg.attendee/meetup-name"];
|
||||
attendeeIDInput.value = attendee["bbbg.attendee/id"];
|
||||
|
||||
submit.classList.remove("hidden");
|
||||
submit.removeAttribute("disabled");
|
||||
signupForm.removeAttribute("disabled");
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("form").forEach((form) => {
|
||||
form.addEventListener("submit", (e) => {
|
||||
if (e.target.attributes.disabled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const confirmMessage = e.target.dataset.confirm;
|
||||
if (confirmMessage != null && !confirm(confirmMessage)) {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
2
users/aspen/bbbg/resources/public/robots.txt
Normal file
2
users/aspen/bbbg/resources/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
29
users/aspen/bbbg/shell.nix
Normal file
29
users/aspen/bbbg/shell.nix
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
let
|
||||
depot = import ../../.. { };
|
||||
in
|
||||
with depot.third_party.nixpkgs;
|
||||
|
||||
mkShell {
|
||||
buildInputs = [
|
||||
arion
|
||||
depot.third_party.clj2nix
|
||||
clojure
|
||||
openjdk11_headless
|
||||
postgresql_12
|
||||
nix-prefetch-git
|
||||
(writeShellScriptBin "terraform" ''
|
||||
set -e
|
||||
module=$(nix-build ~/code/depot -A users.grfn.bbbg.tf.module)
|
||||
rm -f ~/tfstate/bbbg/*.json
|
||||
cp ''${module}/*.json ~/tfstate/bbbg
|
||||
exec ${depot.users.aspen.bbbg.tf.terraform}/bin/terraform \
|
||||
-chdir=/home/grfn/tfstate/bbbg \
|
||||
"$@"
|
||||
'')
|
||||
];
|
||||
|
||||
PGHOST = "localhost";
|
||||
PGUSER = "bbbg";
|
||||
PGDATABASE = "bbbg";
|
||||
PGPASSWORD = "password";
|
||||
}
|
||||
10
users/aspen/bbbg/src/bbbg/attendee.clj
Normal file
10
users/aspen/bbbg/src/bbbg/attendee.clj
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
(ns bbbg.attendee
|
||||
(:require [clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::id uuid?)
|
||||
|
||||
(s/def ::meetup-name (s/and string? seq))
|
||||
|
||||
(s/def ::discord-name (s/nilable string?))
|
||||
|
||||
(s/def ::organizer-notes string?)
|
||||
4
users/aspen/bbbg/src/bbbg/attendee_check.clj
Normal file
4
users/aspen/bbbg/src/bbbg/attendee_check.clj
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
(ns bbbg.attendee-check
|
||||
(:require [clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::id uuid?)
|
||||
69
users/aspen/bbbg/src/bbbg/core.clj
Normal file
69
users/aspen/bbbg/src/bbbg/core.clj
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
(ns bbbg.core
|
||||
(:gen-class)
|
||||
(:require
|
||||
[bbbg.db :as db]
|
||||
[bbbg.web :as web]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.spec.test.alpha :as stest]
|
||||
[com.stuartsierra.component :as component]
|
||||
[expound.alpha :as exp]))
|
||||
|
||||
(s/def ::config
|
||||
(s/merge
|
||||
::db/config
|
||||
::web/config))
|
||||
|
||||
(defn make-system [config]
|
||||
(component/system-map
|
||||
:db (db/make-database config)
|
||||
:web (web/make-server config)))
|
||||
|
||||
(defn env->config []
|
||||
(s/assert
|
||||
::config
|
||||
(merge
|
||||
(db/env->config)
|
||||
(web/env->config))))
|
||||
|
||||
(defn dev-config []
|
||||
(s/assert
|
||||
::config
|
||||
(merge
|
||||
(db/dev-config)
|
||||
(web/dev-config))))
|
||||
|
||||
(defonce system nil)
|
||||
|
||||
(defn init-dev []
|
||||
(s/check-asserts true)
|
||||
(set! s/*explain-out* exp/printer)
|
||||
(stest/instrument))
|
||||
|
||||
(defn run-dev []
|
||||
(init-dev)
|
||||
(alter-var-root
|
||||
#'system
|
||||
(fn [sys]
|
||||
(when sys
|
||||
(component/start sys))
|
||||
(component/start (make-system (dev-config))))))
|
||||
|
||||
(defn -main [& _args]
|
||||
(alter-var-root
|
||||
#'system
|
||||
(constantly (component/start (make-system (env->config))))))
|
||||
|
||||
(comment
|
||||
;; To run the application:
|
||||
;; 1. `M-x cider-jack-in`
|
||||
;; 2. `M-x cider-load-buffer` in this buffer
|
||||
;; 3. (optionally) configure the secrets backend in `bbbg.util.dev-secrets`
|
||||
;; 4. Put your cursor after the following form and run `M-x cider-eval-last-sexp`
|
||||
;;
|
||||
;; A web server will be listening on http://localhost:8888
|
||||
|
||||
(do
|
||||
(run-dev)
|
||||
(bbbg.db/migrate! (:db system)))
|
||||
|
||||
)
|
||||
366
users/aspen/bbbg/src/bbbg/db.clj
Normal file
366
users/aspen/bbbg/src/bbbg/db.clj
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
(ns bbbg.db
|
||||
(:gen-class)
|
||||
(:refer-clojure :exclude [get list count])
|
||||
(:require [camel-snake-kebab.core :as csk :refer [->kebab-case ->snake_case]]
|
||||
[bbbg.util.core :as u]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string :as str]
|
||||
[com.stuartsierra.component :as component]
|
||||
[config.core :refer [env]]
|
||||
[honeysql.format :as hformat]
|
||||
[migratus.core :as migratus]
|
||||
[next.jdbc :as jdbc]
|
||||
[next.jdbc.connection :as jdbc.conn]
|
||||
next.jdbc.date-time
|
||||
[next.jdbc.optional :as jdbc.opt]
|
||||
[next.jdbc.result-set :as rs]
|
||||
[next.jdbc.sql :as sql])
|
||||
(:import [com.impossibl.postgres.jdbc PGSQLSimpleException]
|
||||
com.zaxxer.hikari.HikariDataSource
|
||||
[java.sql Connection ResultSet Types]
|
||||
javax.sql.DataSource))
|
||||
|
||||
(s/def ::host string?)
|
||||
(s/def ::database string?)
|
||||
(s/def ::user string?)
|
||||
(s/def ::password string?)
|
||||
|
||||
(s/def ::config
|
||||
(s/keys :opt [::host
|
||||
::database
|
||||
::user
|
||||
::password]))
|
||||
|
||||
(s/fdef make-database
|
||||
:args
|
||||
(s/cat :config (s/keys :opt [::config])))
|
||||
|
||||
(s/fdef env->config :ret ::config)
|
||||
|
||||
(s/def ::db any?)
|
||||
|
||||
;;;
|
||||
|
||||
(def default-config
|
||||
(s/assert
|
||||
::config
|
||||
{::host "localhost"
|
||||
::database "bbbg"
|
||||
::user "bbbg"
|
||||
::password "password"}))
|
||||
|
||||
(defn dev-config [] default-config)
|
||||
|
||||
(defn env->config []
|
||||
(->>
|
||||
{::host (:pghost env)
|
||||
::database (:pgdatabase env)
|
||||
::user (:pguser env)
|
||||
::password (:pgpassword env)}
|
||||
u/remove-nils
|
||||
(s/assert ::config)))
|
||||
|
||||
(defn ->db-spec [config]
|
||||
(-> default-config
|
||||
(merge config)
|
||||
(set/rename-keys
|
||||
{::host :host
|
||||
::database :dbname
|
||||
::user :username
|
||||
::password :password})
|
||||
(assoc :dbtype "pgsql")))
|
||||
|
||||
(defn connection
|
||||
"Make a one-off connection from the given `::config` map, or the environment
|
||||
if not provided"
|
||||
([] (connection (env->config)))
|
||||
([config]
|
||||
(-> config
|
||||
->db-spec
|
||||
(set/rename-keys {:username :user})
|
||||
jdbc/get-datasource
|
||||
jdbc/get-connection)))
|
||||
|
||||
(defrecord Database [config]
|
||||
component/Lifecycle
|
||||
(start [this]
|
||||
(assoc this :pool (jdbc.conn/->pool HikariDataSource (->db-spec config))))
|
||||
(stop [this]
|
||||
(some-> this :pool .close)
|
||||
(dissoc this :pool))
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [this] (:pool this)))
|
||||
|
||||
(defn make-database [config]
|
||||
(map->Database {:config config}))
|
||||
|
||||
(defn database? [x]
|
||||
(or
|
||||
(instance? Database x)
|
||||
(and (map? x) (contains? x :pool))))
|
||||
|
||||
;;;
|
||||
;;; Migrations
|
||||
;;;
|
||||
|
||||
(defn migratus-config
|
||||
[db]
|
||||
{:store :database
|
||||
:migration-dir "migrations/"
|
||||
:migration-table-name "__migrations__"
|
||||
:db
|
||||
(let [db (if (ifn? db) (db) db)]
|
||||
(cond
|
||||
(.isInstance Connection db)
|
||||
{:connection db}
|
||||
(.isInstance DataSource db)
|
||||
{:datasource db}
|
||||
:else (throw
|
||||
(ex-info "migratus-config called with value of unrecognized type"
|
||||
{:value db}))))})
|
||||
|
||||
(defn generate-migration
|
||||
([db name] (generate-migration db name :sql))
|
||||
([db name type] (migratus/create (migratus-config db) name type)))
|
||||
|
||||
(defn migrate!
|
||||
[db] (migratus/migrate (migratus-config db)))
|
||||
|
||||
(defn rollback!
|
||||
[db] (migratus/rollback (migratus-config db)))
|
||||
|
||||
;;;
|
||||
;;; Database interaction
|
||||
;;;
|
||||
|
||||
(defn ->key-ns [tn]
|
||||
(let [tn (name tn)
|
||||
tn (if (str/starts-with? tn "public.")
|
||||
(second (str/split tn #"\." 2))
|
||||
tn)]
|
||||
(str "bbbg." (->kebab-case tn))))
|
||||
|
||||
(defn ->table-name [kns]
|
||||
(let [kns (name kns)]
|
||||
(->snake_case
|
||||
(if (str/starts-with? kns "public.")
|
||||
kns
|
||||
(str "public." (last (str/split kns #"\.")))))))
|
||||
|
||||
(defn ->column
|
||||
([col] (->column nil col))
|
||||
([table col]
|
||||
(let [col-table (some-> col namespace ->table-name)
|
||||
snake-col (-> col name ->snake_case (str/replace #"\?$" ""))]
|
||||
(if (or (not (namespace col))
|
||||
(not table)
|
||||
(= (->table-name table) col-table))
|
||||
snake-col
|
||||
;; different table, assume fk
|
||||
(str
|
||||
(str/replace-first col-table "public." "")
|
||||
"_"
|
||||
snake-col)))))
|
||||
|
||||
(defn ->value [v]
|
||||
(if (keyword? v)
|
||||
(-> v name csk/->snake_case_string)
|
||||
v))
|
||||
|
||||
(defn process-key-map [table key-map]
|
||||
(into {}
|
||||
(map (fn [[k v]] [(->column table k)
|
||||
(->value v)]))
|
||||
key-map))
|
||||
|
||||
(defn fkize [col]
|
||||
(if (str/ends-with? col "-id")
|
||||
(let [table (str/join "-" (butlast (str/split (name col) #"-")))]
|
||||
(keyword (->key-ns table) "id"))
|
||||
col))
|
||||
|
||||
(def ^:private enum-members-cache (atom {}))
|
||||
(defn- enum-members
|
||||
"Returns a set of enum members as strings for the enum with the given name"
|
||||
[db name]
|
||||
(if-let [e (find @enum-members-cache name)]
|
||||
(val e)
|
||||
(let [r (try
|
||||
(-> (jdbc/execute-one!
|
||||
(db)
|
||||
[(format "select enum_range(null::%s) as members" name)])
|
||||
:members
|
||||
.getArray
|
||||
set)
|
||||
(catch PGSQLSimpleException _
|
||||
nil))]
|
||||
(swap! enum-members-cache assoc name r)
|
||||
r)))
|
||||
|
||||
(def ^{:private true
|
||||
:dynamic true}
|
||||
*meta-db*
|
||||
"Database connection to use to query metadata"
|
||||
nil)
|
||||
|
||||
(extend-protocol rs/ReadableColumn
|
||||
String
|
||||
(read-column-by-label [x _] x)
|
||||
(read-column-by-index [x rsmeta idx]
|
||||
(if-not *meta-db*
|
||||
x
|
||||
(let [typ (.getColumnTypeName rsmeta idx)]
|
||||
;; TODO: Is there a better way to figure out if a type is an enum?
|
||||
(if (enum-members *meta-db* typ)
|
||||
(keyword (csk/->kebab-case-string typ)
|
||||
(csk/->kebab-case-string x))
|
||||
x)))))
|
||||
|
||||
(comment
|
||||
(->key-ns :public.user)
|
||||
(->key-ns :public.api-token)
|
||||
(->key-ns :api-token)
|
||||
(->table-name :api-token)
|
||||
(->table-name :public.user)
|
||||
(->table-name :bbbg.user)
|
||||
)
|
||||
|
||||
(defn as-fq-maps [^ResultSet rs _opts]
|
||||
(let [qualify #(when (seq %) (str "bbbg." (->kebab-case %)))
|
||||
rsmeta (.getMetaData rs)
|
||||
cols (mapv
|
||||
(fn [^Integer i]
|
||||
(let [ty (.getColumnType rsmeta i)
|
||||
lab (.getColumnLabel rsmeta i)
|
||||
n (str (->kebab-case lab)
|
||||
(when (= ty Types/BOOLEAN) "?"))]
|
||||
(fkize
|
||||
(if-let [q (some-> rsmeta (.getTableName i) qualify not-empty)]
|
||||
(keyword q n)
|
||||
(keyword n)))))
|
||||
(range 1 (inc (.getColumnCount rsmeta))))]
|
||||
(jdbc.opt/->MapResultSetOptionalBuilder rs rsmeta cols)))
|
||||
|
||||
(def jdbc-opts
|
||||
{:builder-fn as-fq-maps
|
||||
:column-fn ->snake_case
|
||||
:table-fn ->snake_case})
|
||||
|
||||
(defmethod hformat/fn-handler "count-distinct" [_ field]
|
||||
(str "count(distinct " (hformat/to-sql field) ")"))
|
||||
|
||||
(defn fetch
|
||||
"Fetch a single row from the db matching the given `sql-map` or query"
|
||||
[db sql-map & [opts]]
|
||||
(s/assert
|
||||
(s/nilable (s/keys))
|
||||
(binding [*meta-db* db]
|
||||
(jdbc/execute-one!
|
||||
(db)
|
||||
(if (map? sql-map)
|
||||
(hformat/format sql-map)
|
||||
sql-map)
|
||||
(merge jdbc-opts opts)))))
|
||||
|
||||
(defn get
|
||||
"Retrieve a single record from the given table by ID"
|
||||
[db table id & [opts]]
|
||||
(when id
|
||||
(fetch
|
||||
db
|
||||
{:select [:*]
|
||||
:from [table]
|
||||
:where [:= :id id]}
|
||||
opts)))
|
||||
|
||||
(defn list
|
||||
"Returns a list of rows from the db matching the given sql-map, table or
|
||||
query"
|
||||
[db sql-map-or-table & [opts]]
|
||||
(s/assert
|
||||
(s/coll-of (s/keys))
|
||||
(binding [*meta-db* db]
|
||||
(jdbc/execute!
|
||||
(db)
|
||||
(cond
|
||||
(map? sql-map-or-table)
|
||||
(hformat/format sql-map-or-table)
|
||||
(keyword? sql-map-or-table)
|
||||
(hformat/format {:select [:*] :from [sql-map-or-table]})
|
||||
:else
|
||||
sql-map-or-table)
|
||||
(merge jdbc-opts opts)))))
|
||||
|
||||
(defn count
|
||||
[db sql-map]
|
||||
(binding [*meta-db* db]
|
||||
(:count
|
||||
(fetch db {:select [[:%count.* :count]], :from [[sql-map :sq]]}))))
|
||||
|
||||
(defn exists?
|
||||
"Returns true if the given sql query-map would return any results"
|
||||
[db sql-map]
|
||||
(binding [*meta-db* db]
|
||||
(pos?
|
||||
(count db sql-map))))
|
||||
|
||||
(defn execute!
|
||||
"Given a database and a honeysql query map, perform an operation on the
|
||||
database and discard the results"
|
||||
[db sql-map & [opts]]
|
||||
(jdbc/execute!
|
||||
(db)
|
||||
(hformat/format sql-map)
|
||||
(merge jdbc-opts opts)))
|
||||
|
||||
(defn insert!
|
||||
"Given a database, a table name, and a data hash map, inserts the
|
||||
data as a single row in the database and attempts to return a map of generated
|
||||
keys."
|
||||
[db table key-map & [opts]]
|
||||
(binding [*meta-db* db]
|
||||
(sql/insert!
|
||||
(db)
|
||||
table
|
||||
(process-key-map table key-map)
|
||||
(merge jdbc-opts opts))))
|
||||
|
||||
(defn update!
|
||||
"Given a database, a table name, a hash map of columns and values
|
||||
to set, and a honeysql predicate, perform an update on the table.
|
||||
Will "
|
||||
[db table key-map where-params & [opts]]
|
||||
(binding [*meta-db* db]
|
||||
(execute! db
|
||||
{:update table
|
||||
:set (u/map-keys keyword (process-key-map table key-map))
|
||||
:where where-params
|
||||
:returning [:id]}
|
||||
opts)))
|
||||
|
||||
(defn delete!
|
||||
"Delete all rows from the given table matching the given where clause"
|
||||
[db table where-clause]
|
||||
(binding [*meta-db* db]
|
||||
(sql/delete! (db) table (hformat/format-predicate where-clause))))
|
||||
|
||||
(defmacro with-transaction [[sym db opts] & body]
|
||||
`(jdbc/with-transaction
|
||||
[tx# (~db) ~opts]
|
||||
(let [~sym (constantly tx#)]
|
||||
~@body)))
|
||||
|
||||
(defn -main [& args]
|
||||
(let [db (component/start (make-database (env->config)))]
|
||||
(case (first args)
|
||||
"migrate" (migrate! db)
|
||||
"rollback" (rollback! db))))
|
||||
|
||||
(comment
|
||||
(def db (:db bbbg.core/system))
|
||||
(generate-migration db "add-attendee-unique-meetup-id")
|
||||
(migrate! db)
|
||||
|
||||
)
|
||||
85
users/aspen/bbbg/src/bbbg/db/attendee.clj
Normal file
85
users/aspen/bbbg/src/bbbg/db/attendee.clj
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
(ns bbbg.db.attendee
|
||||
(:require
|
||||
[bbbg.attendee :as attendee]
|
||||
[bbbg.db :as db]
|
||||
[bbbg.util.sql :refer [count-where]]
|
||||
honeysql-postgres.helpers
|
||||
[honeysql.helpers
|
||||
:refer
|
||||
[merge-group-by merge-join merge-left-join merge-select merge-where]]
|
||||
[bbbg.util.core :as u]))
|
||||
|
||||
(defn search
|
||||
([q] (search {:select [:attendee.*] :from [:attendee]} q))
|
||||
([db-or-query q]
|
||||
(if (db/database? db-or-query)
|
||||
(db/list db-or-query (search q))
|
||||
(cond-> db-or-query
|
||||
q (merge-where
|
||||
[:or
|
||||
[:ilike :meetup_name (str "%" q "%")]
|
||||
[:ilike :discord_name (str "%" q "%")]]))))
|
||||
([db query q]
|
||||
(db/list db (search query q))))
|
||||
|
||||
(defn for-event
|
||||
([event-id]
|
||||
(for-event {:select [:attendee.*]
|
||||
:from [:attendee]}
|
||||
event-id))
|
||||
([db-or-query event-id]
|
||||
(if (db/database? db-or-query)
|
||||
(db/list db-or-query (for-event event-id))
|
||||
(-> db-or-query
|
||||
(merge-select :event-attendee.*)
|
||||
(merge-join :event_attendee [:= :attendee.id :event_attendee.attendee_id])
|
||||
(merge-where [:= :event_attendee.event_id event-id]))))
|
||||
([db query event-id]
|
||||
(db/list db (for-event query event-id))))
|
||||
|
||||
(defn with-stats
|
||||
([] (with-stats {:select [:attendee.*]
|
||||
:from [:attendee]}))
|
||||
([query]
|
||||
(-> query
|
||||
(merge-left-join :event_attendee [:= :attendee.id :event_attendee.attendee_id])
|
||||
(merge-group-by :attendee.id)
|
||||
(merge-select
|
||||
[(count-where :event_attendee.rsvpd_attending) :events-rsvpd]
|
||||
[(count-where :event_attendee.attended) :events-attended]
|
||||
[(count-where [:and
|
||||
:event_attendee.rsvpd_attending
|
||||
[:not :event_attendee.attended]])
|
||||
:no-shows]))))
|
||||
|
||||
(defn upsert-all!
|
||||
[db attendees]
|
||||
(when (seq attendees)
|
||||
(db/list
|
||||
db
|
||||
{:insert-into :attendee
|
||||
:values (map #(->> %
|
||||
(db/process-key-map :attendee)
|
||||
(u/map-keys keyword))
|
||||
attendees)
|
||||
:upsert {:on-conflict [:meetup-user-id]
|
||||
:do-update-set [:meetup-name]}
|
||||
:returning [:id :meetup-user-id]})))
|
||||
|
||||
(comment
|
||||
(def db (:db bbbg.core/system))
|
||||
(db/database? db)
|
||||
(search db "gri")
|
||||
(db/insert! db :attendee {::attendee/meetup-name "Griffin Smith"
|
||||
::attendee/discord-name "grfn"
|
||||
})
|
||||
|
||||
(search db (with-stats) "gri")
|
||||
|
||||
(search (with-stats) "gri")
|
||||
|
||||
(db/list db (with-stats))
|
||||
|
||||
(db/insert! db :attendee {::attendee/meetup-name "Rando Guy"
|
||||
::attendee/discord-name "rando"})
|
||||
)
|
||||
55
users/aspen/bbbg/src/bbbg/db/attendee_check.clj
Normal file
55
users/aspen/bbbg/src/bbbg/db/attendee_check.clj
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
(ns bbbg.db.attendee-check
|
||||
(:require
|
||||
[bbbg.attendee :as attendee]
|
||||
[bbbg.attendee-check :as attendee-check]
|
||||
[bbbg.db :as db]
|
||||
[bbbg.user :as user]
|
||||
[bbbg.util.core :as u]))
|
||||
|
||||
(defn create! [db params]
|
||||
(db/insert! db :attendee-check
|
||||
(select-keys params [::attendee/id
|
||||
::user/id
|
||||
::attendee-check/last-dose-at])))
|
||||
|
||||
(defn attendees-with-last-checks
|
||||
[db attendees]
|
||||
(when (seq attendees)
|
||||
(let [ids (map ::attendee/id attendees)
|
||||
checks
|
||||
(db/list db {:select [:attendee-check.*]
|
||||
:from [:attendee-check]
|
||||
:join [[{:select [:%max.attendee-check.checked-at
|
||||
:attendee-check.attendee-id]
|
||||
:from [:attendee-check]
|
||||
:group-by [:attendee-check.attendee-id]
|
||||
:where [:in :attendee-check.attendee-id ids]}
|
||||
:last-check]
|
||||
[:=
|
||||
:attendee-check.attendee-id
|
||||
:last-check.attendee-id]]})
|
||||
users (if (seq checks)
|
||||
(u/key-by
|
||||
::user/id
|
||||
(db/list db {:select [:public.user.*]
|
||||
:from [:public.user]
|
||||
:where [:in :id (map ::user/id checks)]}))
|
||||
{})
|
||||
checks (map #(assoc % :user (users (::user/id %))) checks)
|
||||
attendee-id->check (u/key-by ::attendee/id checks)]
|
||||
(map #(assoc % :last-check (attendee-id->check (::attendee/id %)))
|
||||
attendees))))
|
||||
|
||||
(comment
|
||||
(def db (:db bbbg.core/system))
|
||||
|
||||
(attendees-with-last-checks
|
||||
db
|
||||
(db/list db :attendee)
|
||||
)
|
||||
|
||||
(db/insert! db :attendee-check
|
||||
{::attendee/id #uuid "58bcd372-ff6e-49df-b280-23d24c5ba0f0"
|
||||
::user/id #uuid "303fb606-5ef0-4682-ad7d-6429c670cd78"
|
||||
::attendee-check/last-dose-at "2021-12-19"})
|
||||
)
|
||||
94
users/aspen/bbbg/src/bbbg/db/event.clj
Normal file
94
users/aspen/bbbg/src/bbbg/db/event.clj
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
(ns bbbg.db.event
|
||||
(:require
|
||||
[bbbg.attendee :as attendee]
|
||||
[bbbg.db :as db]
|
||||
[bbbg.event :as event]
|
||||
[bbbg.util.sql :refer [count-where]]
|
||||
[honeysql.helpers
|
||||
:refer [merge-group-by merge-left-join merge-select merge-where]]
|
||||
[java-time :refer [local-date local-date-time local-time]]))
|
||||
|
||||
(defn create! [db event]
|
||||
(db/insert! db :event (select-keys event [::event/date])))
|
||||
|
||||
(defn attended!
|
||||
[db params]
|
||||
(db/execute!
|
||||
db
|
||||
{:insert-into :event-attendee
|
||||
:values [{:event_id (::event/id params)
|
||||
:attendee_id (::attendee/id params)
|
||||
:attended true}]
|
||||
:upsert {:on-conflict [:event-id :attendee-id]
|
||||
:do-update-set! {:attended true}}}))
|
||||
|
||||
(defn on-day
|
||||
([day] {:select [:event.*]
|
||||
:from [:event]
|
||||
:where [:= :date (str day)]})
|
||||
([db day]
|
||||
(db/list db (on-day day))))
|
||||
|
||||
|
||||
(def end-of-day-hour
|
||||
;; 7am utc = 3am nyc
|
||||
7)
|
||||
|
||||
(defn current-day
|
||||
([] (current-day (local-date-time)))
|
||||
([dt]
|
||||
(if (<= 0
|
||||
(.getHour (local-time dt))
|
||||
end-of-day-hour)
|
||||
(java-time/minus
|
||||
(local-date dt)
|
||||
(java-time/days 1))
|
||||
(local-date dt))))
|
||||
|
||||
(comment
|
||||
(current-day
|
||||
(local-date-time
|
||||
2022 5 1
|
||||
1 13 0))
|
||||
)
|
||||
|
||||
(defn today
|
||||
([] (on-day (current-day)))
|
||||
([db] (db/list db (today))))
|
||||
|
||||
(defn upcoming
|
||||
([] (upcoming {:select [:event.*] :from [:event]}))
|
||||
([query]
|
||||
(merge-where query [:>= :date (local-date)])))
|
||||
|
||||
(defn past
|
||||
([] (past {:select [:event.*] :from [:event]}))
|
||||
([query]
|
||||
(merge-where query [:< :date (local-date)])))
|
||||
|
||||
(defn with-attendee-counts
|
||||
[query]
|
||||
(-> query
|
||||
(merge-left-join :event_attendee [:= :event.id :event_attendee.event-id])
|
||||
(merge-select :%count.event_attendee.attendee_id)
|
||||
(merge-group-by :event.id :event_attendee.event-id)))
|
||||
|
||||
(defn with-stats
|
||||
[query]
|
||||
(-> query
|
||||
(merge-left-join :event_attendee [:= :event.id :event_attendee.event-id])
|
||||
(merge-select
|
||||
[(count-where :event-attendee.rsvpd_attending) :num-rsvps]
|
||||
[(count-where :event-attendee.attended) :num-attendees])
|
||||
(merge-group-by :event.id)))
|
||||
|
||||
(comment
|
||||
(def db (:db bbbg.core/system))
|
||||
(db/list db (-> (today) (with-attendee-counts)))
|
||||
|
||||
(honeysql.format/format
|
||||
(honeysql-postgres.helpers/upsert {:insert-into :foo
|
||||
:values {:bar 1}}
|
||||
(-> (honeysql-postgres.helpers/on-conflict :did)
|
||||
(honeysql-postgres.helpers/do-update-set! [:did true]))))
|
||||
)
|
||||
17
users/aspen/bbbg/src/bbbg/db/event_attendee.clj
Normal file
17
users/aspen/bbbg/src/bbbg/db/event_attendee.clj
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
(ns bbbg.db.event-attendee
|
||||
(:require honeysql-postgres.format
|
||||
[bbbg.db :as db]
|
||||
[bbbg.util.core :as u]))
|
||||
|
||||
(defn upsert-all!
|
||||
[db attendees]
|
||||
(when (seq attendees)
|
||||
(db/execute!
|
||||
db
|
||||
{:insert-into :event-attendee
|
||||
:values (map #(->> %
|
||||
(db/process-key-map :event-attendee)
|
||||
(u/map-keys keyword))
|
||||
attendees)
|
||||
:upsert {:on-conflict [:event-id :attendee-id]
|
||||
:do-update-set [:rsvpd-attending]}})))
|
||||
19
users/aspen/bbbg/src/bbbg/db/user.clj
Normal file
19
users/aspen/bbbg/src/bbbg/db/user.clj
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
(ns bbbg.db.user
|
||||
(:require [bbbg.db :as db]
|
||||
[bbbg.user :as user]))
|
||||
|
||||
(defn create! [db attrs]
|
||||
(db/insert! db
|
||||
:public.user
|
||||
(select-keys attrs [::user/id
|
||||
::user/username
|
||||
::user/discord-user-id])))
|
||||
|
||||
(defn find-or-create! [db attrs]
|
||||
(or
|
||||
(db/fetch db {:select [:*]
|
||||
:from [:public.user]
|
||||
:where [:=
|
||||
:discord-user-id
|
||||
(::user/discord-user-id attrs)]})
|
||||
(create! db attrs)))
|
||||
44
users/aspen/bbbg/src/bbbg/discord.clj
Normal file
44
users/aspen/bbbg/src/bbbg/discord.clj
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
(ns bbbg.discord
|
||||
(:refer-clojure :exclude [get])
|
||||
(:require
|
||||
[bbbg.util.dev-secrets :refer [secret]]
|
||||
[clj-http.client :as http]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(def base-uri "https://discord.com/api")
|
||||
|
||||
(defn api-uri [path]
|
||||
(str base-uri
|
||||
(when-not (str/starts-with? path "/") "/")
|
||||
path))
|
||||
|
||||
(defn get
|
||||
([token path]
|
||||
(get token path {}))
|
||||
([token path params]
|
||||
(:body
|
||||
(http/get (api-uri path)
|
||||
(-> params
|
||||
(assoc :accept :json
|
||||
:as :json)
|
||||
(assoc-in [:headers "authorization"]
|
||||
(str "Bearer " (:token token))))))))
|
||||
|
||||
(defn me [token]
|
||||
(get token "/users/@me"))
|
||||
|
||||
(defn guilds [token]
|
||||
(get token "/users/@me/guilds"))
|
||||
|
||||
(defn guild-member [token guild-id]
|
||||
(get token (str "/users/@me/guilds/" guild-id "/member")))
|
||||
|
||||
(comment
|
||||
(def token {:token (secret "bbbg/test-token")})
|
||||
(me token)
|
||||
(guilds token)
|
||||
(guild-member token "841295283564052510")
|
||||
|
||||
(get token "/guilds/841295283564052510/roles")
|
||||
|
||||
)
|
||||
90
users/aspen/bbbg/src/bbbg/discord/auth.clj
Normal file
90
users/aspen/bbbg/src/bbbg/discord/auth.clj
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
(ns bbbg.discord.auth
|
||||
(:require
|
||||
[bbbg.discord :as discord]
|
||||
[bbbg.util.core :as u]
|
||||
[bbbg.util.dev-secrets :refer [secret]]
|
||||
clj-time.coerce
|
||||
[clojure.spec.alpha :as s]
|
||||
[config.core :refer [env]]
|
||||
[ring.middleware.oauth2 :refer [wrap-oauth2]]))
|
||||
|
||||
(s/def ::client-id string?)
|
||||
(s/def ::client-secret string?)
|
||||
(s/def ::bbbg-guild-id string?)
|
||||
(s/def ::bbbg-organizer-role string?)
|
||||
|
||||
(s/def ::config (s/keys :req [::client-id
|
||||
::client-secret
|
||||
::bbbg-guild-id
|
||||
::bbbg-organizer-role]))
|
||||
|
||||
;;;
|
||||
|
||||
(defn env->config []
|
||||
(s/assert
|
||||
::config
|
||||
{::client-id (:discord-client-id env)
|
||||
::client-secret (:discord-client-secret env)
|
||||
::bbbg-guild-id (:bbbg-guild-id env "841295283564052510")
|
||||
::bbbg-organizer-role (:bbbg-organizer-role
|
||||
env
|
||||
;; TODO this might not be the right id
|
||||
"908428000817725470")}))
|
||||
|
||||
(defn dev-config []
|
||||
(s/assert
|
||||
::config
|
||||
{::client-id (secret "bbbg/discord-client-id")
|
||||
::client-secret (secret "bbbg/discord-client-secret")
|
||||
::bbbg-guild-id "841295283564052510"
|
||||
::bbbg-organizer-role "908428000817725470"}))
|
||||
|
||||
;;;
|
||||
|
||||
(def access-token-url
|
||||
"https://discord.com/api/oauth2/token")
|
||||
|
||||
(def authorization-url
|
||||
"https://discord.com/api/oauth2/authorize")
|
||||
|
||||
(def revoke-url
|
||||
"https://discord.com/api/oauth2/token/revoke")
|
||||
|
||||
(def scopes ["guilds"
|
||||
"guilds.members.read"
|
||||
"identify"])
|
||||
|
||||
(defn discord-oauth-profile [{:keys [base-url] :as env}]
|
||||
{:authorize-uri authorization-url
|
||||
:access-token-uri access-token-url
|
||||
:client-id (::client-id env)
|
||||
:client-secret (::client-secret env)
|
||||
:scopes scopes
|
||||
:launch-uri "/auth/discord"
|
||||
:redirect-uri (str base-url "/auth/discord/redirect")
|
||||
:landing-uri (str base-url "/auth/success")})
|
||||
|
||||
(comment
|
||||
(-> "https://bbbg-staging.gws.fyi/auth/login"
|
||||
(java.net.URI/create)
|
||||
(.resolve "https://bbbg.gws.fyi/auth/discord/redirect")
|
||||
str)
|
||||
)
|
||||
|
||||
(defn wrap-discord-auth [handler env]
|
||||
(wrap-oauth2 handler {:discord (discord-oauth-profile env)}))
|
||||
|
||||
(defn check-discord-auth
|
||||
"Check that the user with the given token has the correct level of discord
|
||||
auth"
|
||||
[{::keys [bbbg-guild-id bbbg-organizer-role]} token]
|
||||
(and (some (comp #{bbbg-guild-id} :id)
|
||||
(discord/guilds token))
|
||||
(some #{bbbg-organizer-role}
|
||||
(:roles (discord/guild-member token bbbg-guild-id)))))
|
||||
|
||||
(comment
|
||||
(#'ring.middleware.oauth2/valid-profile?
|
||||
(discord-oauth-profile
|
||||
(dev-config)))
|
||||
)
|
||||
4
users/aspen/bbbg/src/bbbg/event.clj
Normal file
4
users/aspen/bbbg/src/bbbg/event.clj
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
(ns bbbg.event
|
||||
(:require [clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::id uuid?)
|
||||
6
users/aspen/bbbg/src/bbbg/event_attendee.clj
Normal file
6
users/aspen/bbbg/src/bbbg/event_attendee.clj
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
(ns bbbg.event-attendee
|
||||
(:require [clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::attended? boolean?)
|
||||
|
||||
(s/def ::rsvpd-attending? boolean?)
|
||||
68
users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj
Normal file
68
users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
(ns bbbg.handlers.attendee-checks
|
||||
(:require
|
||||
[bbbg.attendee :as attendee]
|
||||
[bbbg.attendee-check :as attendee-check]
|
||||
[bbbg.db :as db]
|
||||
[bbbg.db.attendee-check :as db.attendee-check]
|
||||
[bbbg.handlers.core :refer [page-response wrap-auth-required]]
|
||||
[bbbg.user :as user]
|
||||
[bbbg.util.display :refer [format-date]]
|
||||
[compojure.coercions :refer [as-uuid]]
|
||||
[compojure.core :refer [context GET POST]]
|
||||
[ring.util.response :refer [not-found redirect]]
|
||||
[bbbg.views.flash :as flash]))
|
||||
|
||||
(defn- edit-attendee-checks-page [{:keys [existing-check]
|
||||
attendee-id ::attendee/id}]
|
||||
[:div.page
|
||||
(when existing-check
|
||||
[:p
|
||||
"Already checked on "
|
||||
(-> existing-check ::attendee-check/checked-at format-date)
|
||||
" by "
|
||||
(::user/username existing-check)])
|
||||
[:form.attendee-checks-form
|
||||
{:method :post
|
||||
:action (str "/attendees/" attendee-id "/checks")}
|
||||
[:div.form-group
|
||||
[:label
|
||||
"Last Dose"
|
||||
[:input {:type :date
|
||||
:name :last-dose-at}]]]
|
||||
[:div.form-group
|
||||
[:input {:type :submit
|
||||
:value "Mark Checked"}]]]])
|
||||
|
||||
(defn attendee-checks-routes [{:keys [db]}]
|
||||
(wrap-auth-required
|
||||
(context "/attendees/:attendee-id/checks" [attendee-id :<< as-uuid]
|
||||
(GET "/edit" []
|
||||
(if (db/exists? db {:select [1]
|
||||
:from [:attendee]
|
||||
:where [:= :id attendee-id]})
|
||||
(let [existing-check (db/fetch
|
||||
db
|
||||
{:select [:attendee-check.*
|
||||
:public.user.*]
|
||||
:from [:attendee-check]
|
||||
:join [:public.user
|
||||
[:=
|
||||
:attendee-check.user-id
|
||||
:public.user.id]]
|
||||
:where [:= :attendee-id attendee-id]})]
|
||||
(page-response
|
||||
(edit-attendee-checks-page
|
||||
{:existing-check existing-check
|
||||
::attendee/id attendee-id})))
|
||||
(not-found "Attendee not found")))
|
||||
(POST "/" {{:keys [last-dose-at]} :params
|
||||
{user-id ::user/id} :session}
|
||||
(db.attendee-check/create!
|
||||
db
|
||||
{::attendee/id attendee-id
|
||||
::user/id user-id
|
||||
::attendee-check/last-dose-at last-dose-at})
|
||||
(-> (redirect "/attendees")
|
||||
(flash/add-flash
|
||||
#:flash{:type :success
|
||||
:message "Successfully updated vaccination status"}))))))
|
||||
162
users/aspen/bbbg/src/bbbg/handlers/attendees.clj
Normal file
162
users/aspen/bbbg/src/bbbg/handlers/attendees.clj
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
(ns bbbg.handlers.attendees
|
||||
(:require
|
||||
[bbbg.attendee :as attendee]
|
||||
[bbbg.attendee-check :as attendee-check]
|
||||
[bbbg.db :as db]
|
||||
[bbbg.db.attendee :as db.attendee]
|
||||
[bbbg.db.attendee-check :as db.attendee-check]
|
||||
[bbbg.db.event :as db.event]
|
||||
[bbbg.event :as event]
|
||||
[bbbg.handlers.core :refer [page-response wrap-auth-required]]
|
||||
[bbbg.user :as user]
|
||||
[bbbg.util.display :refer [format-date]]
|
||||
[bbbg.views.flash :as flash]
|
||||
[cheshire.core :as json]
|
||||
[compojure.coercions :refer [as-uuid]]
|
||||
[compojure.core :refer [GET POST routes]]
|
||||
[honeysql.helpers :refer [merge-where]]
|
||||
[ring.util.response :refer [content-type not-found redirect response]])
|
||||
(:import
|
||||
java.util.UUID))
|
||||
|
||||
(defn- attendees-page [{:keys [attendees q edit-notes]}]
|
||||
[:div.page
|
||||
[:form.search-form {:method :get :action "/attendees"}
|
||||
[:input.search-input
|
||||
{:type "search"
|
||||
:name "q"
|
||||
:value q
|
||||
:title "Search Attendees"}]
|
||||
[:input {:type "submit"
|
||||
:value "Search Attendees"}]]
|
||||
[:table.attendees
|
||||
[:thead
|
||||
[:tr
|
||||
[:th "Meetup Name"]
|
||||
[:th "Discord Name"]
|
||||
[:th "Events RSVPd"]
|
||||
[:th "Events Attended"]
|
||||
[:th "No-Shows"]
|
||||
[:th "Last Vaccination Check"]
|
||||
[:th "Notes"]]]
|
||||
[:tbody
|
||||
(for [attendee (sort-by
|
||||
(comp #{edit-notes} ::attendee/id)
|
||||
(comp - compare)
|
||||
attendees)
|
||||
:let [id (::attendee/id attendee)]]
|
||||
[:tr
|
||||
[:td.attendee-name (::attendee/meetup-name attendee)]
|
||||
[:td
|
||||
[:label.mobile-label "Discord Name: "]
|
||||
(or (not-empty (::attendee/discord-name attendee))
|
||||
"—")]
|
||||
[:td
|
||||
[:label.mobile-label "Events RSVPd: "]
|
||||
(:events-rsvpd attendee)]
|
||||
[:td
|
||||
[:label.mobile-label "Events Attended: "]
|
||||
(:events-attended attendee)]
|
||||
[:td
|
||||
[:label.mobile-label "No-shows: "]
|
||||
(:no-shows attendee)]
|
||||
[:td
|
||||
[:label.mobile-label "Last Vaccination Check: "]
|
||||
(if-let [last-check (:last-check attendee)]
|
||||
(str "✔️ "(-> last-check
|
||||
::attendee-check/checked-at
|
||||
format-date)
|
||||
", by "
|
||||
(get-in last-check [:user ::user/username]))
|
||||
(list
|
||||
[:span {:title "Not Checked"}
|
||||
"❌"]
|
||||
" "
|
||||
[:a {:href (str "/attendees/" id "/checks/edit")}
|
||||
"Edit"] ))]
|
||||
(if (= edit-notes id)
|
||||
[:td
|
||||
[:form.organizer-notes {:method :post
|
||||
:action (str "/attendees/" id "/notes")}
|
||||
[:div.form-group
|
||||
[:input {:type :text :name "notes"
|
||||
:value (::attendee/organizer-notes attendee)
|
||||
:autofocus true}]]
|
||||
[:div.form-group
|
||||
[:input {:type "Submit" :value "Save Notes"}]]]]
|
||||
[:td
|
||||
[:p
|
||||
(::attendee/organizer-notes attendee)]
|
||||
[:p
|
||||
[:a {:href (str "/attendees?edit-notes=" id)}
|
||||
"Edit Notes"]]])])]]])
|
||||
|
||||
(defn attendees-routes [{:keys [db]}]
|
||||
(routes
|
||||
(wrap-auth-required
|
||||
(routes
|
||||
(GET "/attendees" [q edit-notes]
|
||||
(let [attendees (db/list db (cond-> (db.attendee/with-stats)
|
||||
q (db.attendee/search q)))
|
||||
attendees (db.attendee-check/attendees-with-last-checks
|
||||
db
|
||||
attendees)
|
||||
edit-notes (some-> edit-notes UUID/fromString)]
|
||||
(page-response (attendees-page {:attendees attendees
|
||||
:q q
|
||||
:edit-notes edit-notes}))))
|
||||
|
||||
(POST "/attendees/:id/notes" [id :<< as-uuid notes]
|
||||
(if (seq (db/update! db
|
||||
:attendee
|
||||
{::attendee/organizer-notes notes}
|
||||
[:= :id id]))
|
||||
(-> (redirect "/attendees")
|
||||
(flash/add-flash
|
||||
#:flash{:type :success
|
||||
:message "Notes updated successfully"}))
|
||||
(not-found "Attendee not found")))))
|
||||
|
||||
(GET "/attendees.json" [q event_id attended]
|
||||
(let [results
|
||||
(db/list
|
||||
db
|
||||
(cond->
|
||||
(if q
|
||||
(db.attendee/search q)
|
||||
{:select [:attendee.*] :from [:attendee]})
|
||||
event_id (db.attendee/for-event event_id)
|
||||
(some? attended)
|
||||
(merge-where
|
||||
(case attended
|
||||
"true" :attended
|
||||
"false" [:or [:= :attended nil] [:not :attended]]))))]
|
||||
(-> {:results results}
|
||||
json/generate-string
|
||||
response
|
||||
(content-type "application/json"))))
|
||||
|
||||
(POST "/event_attendees" [event_id attendee_id]
|
||||
(if (and (db/exists? db {:select [:id] :from [:event] :where [:= :id event_id]})
|
||||
(db/exists? db {:select [:id] :from [:attendee] :where [:= :id attendee_id]}))
|
||||
(do
|
||||
(db.event/attended! db {::event/id event_id
|
||||
::attendee/id attendee_id})
|
||||
(-> (redirect (str "/signup-forms/" event_id))
|
||||
(flash/add-flash
|
||||
#:flash{:type :success
|
||||
:message "Thank you for signing in! Enjoy the event."})))
|
||||
(response "Something went wrong")))))
|
||||
|
||||
(comment
|
||||
(def db (:db bbbg.core/system))
|
||||
(db/list db :attendee)
|
||||
(db/list db
|
||||
(->
|
||||
(db.attendee/search "gr")
|
||||
(db.attendee/for-event #uuid "9f4f3eae-3317-41a7-843c-81bcae52aebf")))
|
||||
(honeysql.format/format
|
||||
(->
|
||||
(db.attendee/search "gr")
|
||||
(db.attendee/for-event #uuid "9f4f3eae-3317-41a7-843c-81bcae52aebf")))
|
||||
)
|
||||
91
users/aspen/bbbg/src/bbbg/handlers/core.clj
Normal file
91
users/aspen/bbbg/src/bbbg/handlers/core.clj
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
(ns bbbg.handlers.core
|
||||
(:require
|
||||
[bbbg.user :as user]
|
||||
[bbbg.views.flash :as flash]
|
||||
[hiccup.core :refer [html]]
|
||||
[ring.util.response :refer [content-type response]]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(def ^:dynamic *authenticated?* false)
|
||||
|
||||
(defn authenticated? [request]
|
||||
(some? (get-in request [:session ::user/id])))
|
||||
|
||||
(defn wrap-auth-required [handler]
|
||||
(fn [req]
|
||||
(when (authenticated? req)
|
||||
(handler req))))
|
||||
|
||||
(defn wrap-dynamic-auth [handler]
|
||||
(fn [req]
|
||||
(binding [*authenticated?* (authenticated? req)]
|
||||
(handler req))))
|
||||
|
||||
(def ^:dynamic *current-uri*)
|
||||
|
||||
(defn wrap-current-uri [handler]
|
||||
(fn [req]
|
||||
(binding [*current-uri* (:uri req)]
|
||||
(handler req))))
|
||||
|
||||
(defn nav-item [href label]
|
||||
(let [active?
|
||||
(when *current-uri*
|
||||
(str/starts-with?
|
||||
*current-uri*
|
||||
href))]
|
||||
[:li {:class (when active? "active")}
|
||||
[:a {:href href}
|
||||
label]]))
|
||||
|
||||
(defn global-nav []
|
||||
[:nav.global-nav
|
||||
[:ul
|
||||
(nav-item "/events" "Events")
|
||||
(when *authenticated?*
|
||||
(nav-item "/attendees" "Attendees"))
|
||||
[:li.spacer]
|
||||
[:li
|
||||
(if *authenticated?*
|
||||
[:form.link-form
|
||||
{:method :post
|
||||
:action "/auth/sign-out"}
|
||||
[:input {:type "submit"
|
||||
:value "Sign Out"}]]
|
||||
[:a {:href "/auth/discord"}
|
||||
"Sign In"])]]])
|
||||
|
||||
(defn render-page [opts & body]
|
||||
(let [[{:keys [title]} body]
|
||||
(if (map? opts)
|
||||
[opts body]
|
||||
[{} (concat [opts] body)])]
|
||||
(html
|
||||
[:html {:lang "en"}
|
||||
[:head
|
||||
[:meta {:charset "UTF-8"}]
|
||||
[:meta {:name "viewport"
|
||||
:content "width=device-width,initial-scale=1"}]
|
||||
[:title (if title
|
||||
(str title " - BBBG")
|
||||
"BBBG")]
|
||||
[:link {:rel "stylesheet"
|
||||
:type "text/css"
|
||||
:href "/main.css"}]]
|
||||
[:body
|
||||
[:div.content
|
||||
(global-nav)
|
||||
#_(flash/render-flash flash/test-flash)
|
||||
(flash/render-flash)
|
||||
body]
|
||||
[:script {:src "/main.js"}]]])))
|
||||
|
||||
(defn page-response [& render-page-args]
|
||||
(-> (apply render-page render-page-args)
|
||||
response
|
||||
(content-type "text/html")))
|
||||
|
||||
(comment
|
||||
(render-page
|
||||
[:h1 "hi"])
|
||||
)
|
||||
259
users/aspen/bbbg/src/bbbg/handlers/events.clj
Normal file
259
users/aspen/bbbg/src/bbbg/handlers/events.clj
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
(ns bbbg.handlers.events
|
||||
(:require
|
||||
[bbbg.db :as db]
|
||||
[bbbg.db.attendee :as db.attendee]
|
||||
[bbbg.db.event :as db.event]
|
||||
[bbbg.event :as event]
|
||||
[bbbg.handlers.core :refer [*authenticated?* page-response]]
|
||||
[bbbg.meetup.import :refer [import-attendees!]]
|
||||
[bbbg.util.display :refer [format-date pluralize]]
|
||||
[bbbg.util.time :as t]
|
||||
[bbbg.views.flash :as flash]
|
||||
[compojure.coercions :refer [as-uuid]]
|
||||
[compojure.core :refer [context GET POST]]
|
||||
[java-time :refer [local-date]]
|
||||
[ring.util.response :refer [not-found redirect]]
|
||||
[bbbg.attendee :as attendee]
|
||||
[bbbg.event-attendee :as event-attendee]
|
||||
[bbbg.db.attendee-check :as db.attendee-check]
|
||||
[bbbg.attendee-check :as attendee-check]
|
||||
[bbbg.user :as user])
|
||||
(:import
|
||||
java.time.format.FormatStyle))
|
||||
|
||||
(defn- num-attendees [event]
|
||||
(str
|
||||
(:num-attendees event)
|
||||
(if (= (t/->LocalDate (::event/date event))
|
||||
(local-date))
|
||||
" Signed In"
|
||||
(str " Attendee" (when-not (= 1 (:num-attendees event)) "s")))))
|
||||
|
||||
(def index-type->label
|
||||
{:upcoming "Upcoming"
|
||||
:past "Past"})
|
||||
(def other-index-type
|
||||
{:upcoming :past
|
||||
:past :upcoming})
|
||||
|
||||
(defn events-index
|
||||
[{:keys [events num-events type]}]
|
||||
[:div.page
|
||||
[:div.page-header
|
||||
[:h1
|
||||
(pluralize
|
||||
num-events
|
||||
(str (index-type->label type) " Event"))]
|
||||
[:a {:href (str "/events"
|
||||
(when (= :upcoming type)
|
||||
"/past"))}
|
||||
"View "
|
||||
(index-type->label (other-index-type type))
|
||||
" Events"]]
|
||||
(when *authenticated?*
|
||||
[:a.button {:href "/events/new"}
|
||||
"Create New Event"])
|
||||
[:ul.events-list
|
||||
(for [event (sort-by
|
||||
::event/date
|
||||
(comp - compare)
|
||||
events)]
|
||||
[:li
|
||||
[:p
|
||||
[:a {:href (str "/events/" (::event/id event))}
|
||||
(format-date (::event/date event)
|
||||
FormatStyle/FULL)]]
|
||||
[:p
|
||||
(pluralize (:num-rsvps event) "RSVP")
|
||||
", "
|
||||
(num-attendees event)]])]])
|
||||
|
||||
(defn- import-attendee-list-form-group []
|
||||
[:div.form-group
|
||||
[:label "Import Attendee List"
|
||||
[:br]
|
||||
[:input {:type :file
|
||||
:name :attendees}]]])
|
||||
|
||||
(defn import-attendees-form [event]
|
||||
[:form {:method :post
|
||||
:action (str "/events/" (::event/id event) "/attendees")
|
||||
:enctype "multipart/form-data"}
|
||||
(import-attendee-list-form-group)
|
||||
[:div.form-group
|
||||
[:input {:type :submit
|
||||
:value "Import"}]]])
|
||||
|
||||
(defn event-page [{:keys [event attendees]}]
|
||||
[:div.page
|
||||
[:div.page-header
|
||||
[:h1 (format-date (::event/date event)
|
||||
FormatStyle/FULL)]
|
||||
[:div.spacer]
|
||||
[:a.button {:href (str "/signup-forms/" (::event/id event) )}
|
||||
"Go to Signup Form"]
|
||||
[:form#delete-event
|
||||
{:method :post
|
||||
:action (str "/events/" (::event/id event) "/delete")
|
||||
:data-confirm "Are you sure you want to delete this event?"}
|
||||
[:input.error {:type "submit"
|
||||
:value "Delete Event"}]]]
|
||||
[:div.stats
|
||||
[:p (pluralize (:num-rsvps event) "RSVP")]
|
||||
[:p (num-attendees event)]]
|
||||
[:div
|
||||
(import-attendees-form event)]
|
||||
[:div
|
||||
[:table.attendees
|
||||
[:thead
|
||||
[:th "Meetup Name"]
|
||||
[:th "Discord Name"]
|
||||
[:th "RSVP"]
|
||||
[:th "Signed In"]
|
||||
[:th "Last Vaccination Check"]]
|
||||
[:tbody
|
||||
(for [attendee (sort-by (juxt (comp not ::event-attendee/rsvpd-attending?)
|
||||
(comp not ::event-attendee/attended?)
|
||||
(comp some? :last-check)
|
||||
::attendee/meetup-name)
|
||||
attendees)]
|
||||
[:tr
|
||||
[:td.attendee-name (::attendee/meetup-name attendee)]
|
||||
[:td
|
||||
[:label.mobile-label "Discord Name: "]
|
||||
(or (not-empty (::attendee/discord-name attendee))
|
||||
"—")]
|
||||
[:td
|
||||
[:label.mobile-label "RSVP: "]
|
||||
(if (::event-attendee/rsvpd-attending? attendee)
|
||||
[:span {:title "Yes"} "✔️"]
|
||||
[:span {:title "No"} "❌"])]
|
||||
[:td
|
||||
[:label.mobile-label "Signed In: "]
|
||||
(if (::event-attendee/attended? attendee)
|
||||
[:span {:title "Yes"} "✔️"]
|
||||
[:span {:title "No"} "❌"])]
|
||||
[:td
|
||||
[:label.mobile-label "Last Vaccination Check: "]
|
||||
(if-let [last-check (:last-check attendee)]
|
||||
(str "✔️ "(-> last-check
|
||||
::attendee-check/checked-at
|
||||
format-date)
|
||||
", by "
|
||||
(get-in last-check [:user ::user/username]))
|
||||
(list
|
||||
[:span {:title "Not Checked"}
|
||||
"❌"]
|
||||
" "
|
||||
[:a {:href (str "/attendees/"
|
||||
(::attendee/id attendee)
|
||||
"/checks/edit")}
|
||||
"Edit"]))]])]]]])
|
||||
|
||||
(defn import-attendees-page [{:keys [event]}]
|
||||
[:div.page
|
||||
[:h1 "Import Attendees for " (format-date (::event/date event))]
|
||||
(import-attendees-form event)])
|
||||
|
||||
(defn event-form
|
||||
([] (event-form {}))
|
||||
([event]
|
||||
[:div.page
|
||||
[:div.page-header
|
||||
[:h1 "Create New Event"]]
|
||||
[:form {:method "POST"
|
||||
:action "/events"
|
||||
:enctype "multipart/form-data"}
|
||||
[:div.form-group
|
||||
[:label "Date"
|
||||
[:input {:type "date"
|
||||
:id "date"
|
||||
:name "date"
|
||||
:value (str (::event/date event))}]]]
|
||||
(import-attendee-list-form-group)
|
||||
[:div.form-group
|
||||
[:input {:type "submit"
|
||||
:value "Create Event"}]]]]))
|
||||
|
||||
(defn- events-list-handler [db query type]
|
||||
(let [events (db/list db (db.event/with-stats query))
|
||||
num-events (db/count db query)]
|
||||
(page-response
|
||||
(events-index {:events events
|
||||
:num-events num-events
|
||||
:type type}))))
|
||||
|
||||
(defn events-routes [{:keys [db]}]
|
||||
(context "/events" []
|
||||
(GET "/" []
|
||||
(events-list-handler db (db.event/upcoming) :upcoming))
|
||||
|
||||
(GET "/past" []
|
||||
(events-list-handler db (db.event/past) :past))
|
||||
|
||||
(GET "/new" [date]
|
||||
(page-response
|
||||
{:title "New Event"}
|
||||
(event-form {::event/date date})))
|
||||
|
||||
(POST "/" [date attendees]
|
||||
(let [event (db.event/create! db {::event/date date})
|
||||
message
|
||||
(if attendees
|
||||
(let [num-attendees
|
||||
(import-attendees! db
|
||||
(::event/id event)
|
||||
(:tempfile attendees))]
|
||||
(format "Event created with %d attendees"
|
||||
num-attendees))
|
||||
"Event created")]
|
||||
(-> (str "/signup-forms/" (::event/id event))
|
||||
redirect
|
||||
(flash/add-flash {:flash/type :success
|
||||
:flash/message message}))))
|
||||
|
||||
(context "/:id" [id :<< as-uuid]
|
||||
(GET "/" []
|
||||
(if-let [event (db/fetch db
|
||||
(-> {:select [:event.*]
|
||||
:from [:event]
|
||||
:where [:= :event.id id]}
|
||||
(db.event/with-stats)))]
|
||||
(let [attendees (db.attendee-check/attendees-with-last-checks
|
||||
db
|
||||
(db/list db (db.attendee/for-event id)))]
|
||||
(page-response
|
||||
(event-page {:event event
|
||||
:attendees attendees})))
|
||||
(not-found "Event Not Found")))
|
||||
|
||||
(POST "/delete" []
|
||||
(db/delete! db :event_attendee [:= :event-id id])
|
||||
(db/delete! db :event [:= :id id])
|
||||
(-> (redirect "/events")
|
||||
(flash/add-flash
|
||||
#:flash {:type :success
|
||||
:message "Successfully deleted event"})))
|
||||
|
||||
(GET "/attendees/import" []
|
||||
(if-let [event (db/get db :event id)]
|
||||
(page-response
|
||||
(import-attendees-page {:event event}))
|
||||
(not-found "Event Not Found")))
|
||||
|
||||
(POST "/attendees" [attendees]
|
||||
(let [num-imported (import-attendees! db id (:tempfile attendees))]
|
||||
(-> (redirect (str "/events/" id))
|
||||
(flash/add-flash
|
||||
#:flash{:type :success
|
||||
:message (format "Successfully imported %d attendees"
|
||||
num-imported)})))))))
|
||||
|
||||
(comment
|
||||
(def db (:db bbbg.core/system))
|
||||
|
||||
(-> (db/list db :event)
|
||||
first
|
||||
::event/date
|
||||
format-date)
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue