diff --git a/Cargo.lock b/Cargo.lock index fdeb2567b8..8a2287dd0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,9 +15,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi", + "winapi 0.3.8", ] +[[package]] +name = "arc-swap" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663a8e9a99154b5fb793032533f6328da35e23aac63d5c152279aa8ba356825" + [[package]] name = "arrayvec" version = "0.5.1" @@ -32,7 +38,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.8", ] [[package]] @@ -124,6 +130,35 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "clap" +version = "3.0.0-beta.1" +source = "git+https://github.com/rtfeldman/clap#e1d83a78804a271b053d4d21f69b67f7eb01ad01" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.1" +source = "git+https://github.com/rtfeldman/clap#e1d83a78804a271b053d4d21f69b67f7eb01ad01" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -141,7 +176,7 @@ checksum = "1fc755679c12bda8e5523a71e4d654b6bf2e14bd838dfc48cde6559a05caf7d1" dependencies = [ "atty", "cast", - "clap", + "clap 2.33.0", "criterion-plot", "csv", "itertools", @@ -277,6 +312,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + [[package]] name = "getrandom" version = "0.1.14" @@ -288,6 +339,15 @@ dependencies = [ "wasi", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.10" @@ -325,6 +385,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "indexmap" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" +dependencies = [ + "autocfg 1.0.0", +] + [[package]] name = "indoc" version = "0.3.5" @@ -378,6 +447,15 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb6ee2a7da03bfc3b66ca47c92c2e392fcc053ea040a85561749b026f7aad09a" +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "itertools" version = "0.8.2" @@ -402,6 +480,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -410,9 +498,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.68" +version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" +checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" [[package]] name = "llvm-sys" @@ -429,9 +517,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" dependencies = [ "scopeguard", ] @@ -472,6 +560,81 @@ dependencies = [ "autocfg 1.0.0", ] +[[package]] +name = "mio" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" +dependencies = [ + "cfg-if", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.1", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3" +dependencies = [ + "log", + "mio", + "miow 0.3.3", + "winapi 0.3.8", +] + +[[package]] +name = "mio-uds" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396aa0f2003d7df8395cb93e09871561ccc3e785f0acb369170e8cc74ddf9226" +dependencies = [ + "socket2", + "winapi 0.3.8", +] + +[[package]] +name = "net2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" +dependencies = [ + "cfg-if", + "libc", + "winapi 0.3.8", +] + [[package]] name = "num-traits" version = "0.2.11" @@ -505,9 +668,9 @@ checksum = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405" [[package]] name = "parking_lot" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" dependencies = [ "lock_api", "parking_lot_core", @@ -515,16 +678,16 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1" +checksum = "0e136c1904604defe99ce5fd71a28d473fa60a12255d511aa78a9ddf11237aeb" dependencies = [ "cfg-if", "cloudabi", "libc", "redox_syscall", "smallvec", - "winapi", + "winapi 0.3.8", ] [[package]] @@ -561,6 +724,32 @@ dependencies = [ "difference", ] +[[package]] +name = "proc-macro-error" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", + "syn-mid", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.15" @@ -642,7 +831,7 @@ dependencies = [ "rand_os", "rand_pcg", "rand_xorshift", - "winapi", + "winapi 0.3.8", ] [[package]] @@ -737,7 +926,7 @@ checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" dependencies = [ "libc", "rand_core 0.4.2", - "winapi", + "winapi 0.3.8", ] [[package]] @@ -751,7 +940,7 @@ dependencies = [ "libc", "rand_core 0.4.2", "rdrand", - "winapi", + "winapi 0.3.8", ] [[package]] @@ -854,7 +1043,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" dependencies = [ - "winapi", + "winapi 0.3.8", ] [[package]] @@ -862,15 +1051,22 @@ name = "roc-cli" version = "0.1.0" dependencies = [ "bumpalo", + "clap 3.0.0-beta.1", "im", "im-rc", + "indoc", "inkwell", "inlinable_string", + "maplit", + "pretty_assertions", + "quickcheck", + "quickcheck_macros", "roc_builtins", "roc_can", "roc_collections", "roc_constrain", "roc_gen", + "roc_load", "roc_module", "roc_mono", "roc_parse", @@ -882,6 +1078,7 @@ dependencies = [ "roc_unify", "roc_uniq", "target-lexicon", + "tokio", ] [[package]] @@ -1286,6 +1483,16 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook-registry" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" +dependencies = [ + "arc-swap", + "libc", +] + [[package]] name = "sized-chunks" version = "0.5.3" @@ -1297,10 +1504,34 @@ dependencies = [ ] [[package]] -name = "smallvec" -version = "1.2.0" +name = "slab" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05720e22615919e4734f6a99ceae50d00226c3c5aca406e102ebc33298214e0a" + +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.8", +] + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" @@ -1324,6 +1555,17 @@ dependencies = [ "unicode-xid 0.2.0", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + [[package]] name = "target-lexicon" version = "0.10.0" @@ -1341,7 +1583,7 @@ dependencies = [ "rand 0.7.3", "redox_syscall", "remove_dir_all", - "winapi", + "winapi 0.3.8", ] [[package]] @@ -1383,15 +1625,22 @@ dependencies = [ [[package]] name = "tokio" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee5a0dd887e37d37390c13ff8ac830f992307fe30a1fff0ab8427af67211ba28" +checksum = "34ef16d072d2b6dc8b4a56c70f5c5ced1a37752116f8e7c1e80c659aa7cb6713" dependencies = [ "bytes", "fnv", + "lazy_static", + "libc", "memchr", + "mio", + "mio-named-pipes", + "mio-uds", "num_cpus", "pin-project-lite", + "signal-hook-registry", + "winapi 0.3.8", ] [[package]] @@ -1402,9 +1651,15 @@ checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" [[package]] name = "typenum" -version = "1.11.2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" [[package]] name = "unicode-width" @@ -1430,6 +1685,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63f18aa3b0e35fed5a0048f029558b1518095ffe2a0a31fb87c93dece93a4993" +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" + [[package]] name = "ven_ena" version = "0.13.1" @@ -1469,7 +1730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" dependencies = [ "same-file", - "winapi", + "winapi 0.3.8", "winapi-util", ] @@ -1543,6 +1804,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.8" @@ -1553,6 +1820,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1565,7 +1838,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e" dependencies = [ - "winapi", + "winapi 0.3.8", ] [[package]] @@ -1574,6 +1847,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "wyhash" version = "0.3.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7a4e0b8912..95a1286ce7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,6 +16,21 @@ path = "src/main.rs" test = false bench = false +[features] +default = ["target-x86"] + +target-x86 = [] + +# arm and wasm give linker errors on some platforms +target-arm = [] +target-webassembly = [] + +target-all = [ + "target-x86", + "target-arm", + "target-webassembly" +] + [dependencies] roc_collections = { path = "../compiler/collections" } @@ -31,12 +46,16 @@ roc_uniq = { path = "../compiler/uniq" } roc_unify = { path = "../compiler/unify" } roc_solve = { path = "../compiler/solve" } roc_mono = { path = "../compiler/mono" } +roc_load = { path = "../compiler/load", version = "0.1.0" } roc_gen = { path = "../compiler/gen", version = "0.1.0" } roc_reporting = { path = "../compiler/reporting", version = "0.1.0" } + # TODO switch to clap 3.0.0 once it's out. Tried adding clap = "~3.0.0-beta.1" and cargo wouldn't accept it +clap = { git = "https://github.com/rtfeldman/clap", branch = "master" } im = "14" # im and im-rc should always have the same version! im-rc = "14" # im and im-rc should always have the same version! bumpalo = { version = "3.2", features = ["collections"] } inlinable_string = "0.1.0" +tokio = { version = "0.2", features = ["blocking", "fs", "sync", "rt-threaded", "process", "io-driver"] } # NOTE: rtfeldman/inkwell is a fork of TheDan64/inkwell which does not change anything. # # The reason for this fork is that the way Inkwell is designed, you have to use @@ -56,3 +75,10 @@ inlinable_string = "0.1.0" # This way, GitHub Actions works and nobody's builds get broken. inkwell = { git = "https://github.com/rtfeldman/inkwell", tag = "llvm10-0.release1" } target-lexicon = "0.10" + +[dev-dependencies] +pretty_assertions = "0.5.1" +maplit = "1.0.1" +indoc = "0.3.3" +quickcheck = "0.8" +quickcheck_macros = "0.8" diff --git a/cli/README.md b/cli/README.md deleted file mode 100644 index d17d599d59..0000000000 --- a/cli/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Interpreter - -Usage: - -``` -$ roc FILENAME.roc -``` - -When building from Rust source, use `cargo run -- FILENAME.roc` instead of `roc FILENAME.roc`. - -For example, here's how to run the "EchoName" example this way: - -``` -$ cargo run -- examples/EchoName.roc -``` diff --git a/cli/src/main.rs b/cli/src/main.rs index c680b7ba02..0be3741de3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,7 +1,7 @@ extern crate roc_gen; extern crate roc_reporting; - -use crate::helpers::{infer_expr, uniq_expr_with}; +#[macro_use] +extern crate clap; use bumpalo::Bump; use inkwell::context::Context; use inkwell::module::Linkage; @@ -9,118 +9,236 @@ use inkwell::passes::PassManager; use inkwell::types::BasicType; use inkwell::OptimizationLevel; use roc_collections::all::ImMap; +use roc_collections::all::MutMap; use roc_gen::llvm::build::{ - build_proc, build_proc_header, get_call_conventions, module_from_builtins, + build_proc, build_proc_header, get_call_conventions, module_from_builtins, OptLevel, }; use roc_gen::llvm::convert::basic_type_from_layout; +use roc_load::file::{LoadedModule, LoadingProblem}; +use roc_module::symbol::Symbol; use roc_mono::expr::{Expr, Procs}; use roc_mono::layout::Layout; use std::time::SystemTime; +use clap::{App, Arg, ArgMatches}; use inkwell::targets::{ CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetTriple, }; -use std::fs::File; -use std::io; -use std::io::prelude::*; +use std::io::{self, ErrorKind}; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process; use target_lexicon::{Architecture, OperatingSystem, Triple, Vendor}; +use tokio::process::Command; +use tokio::runtime::Builder; -pub mod helpers; +pub mod repl; + +pub static FLAG_OPTIMIZE: &str = "optimize"; +pub static FLAG_ROC_FILE: &str = "ROC_FILE"; + +pub fn build_app<'a>() -> App<'a> { + App::new("roc") + .version(crate_version!()) + .subcommand(App::new("build") + .about("Build a program") + .arg( + Arg::with_name(FLAG_ROC_FILE) + .help("The .roc file to build") + .required(true), + ) + .arg( + Arg::with_name(FLAG_OPTIMIZE) + .long(FLAG_OPTIMIZE) + .help("Optimize the compiled program to run faster. (Optimization takes time to complete.)") + .required(false), + ) + ) + .subcommand(App::new("run") + .about("Build and run a program") + .arg( + Arg::with_name(FLAG_ROC_FILE) + .help("The .roc file to build and run") + .required(true), + ) + .arg( + Arg::with_name(FLAG_OPTIMIZE) + .long(FLAG_OPTIMIZE) + .help("Optimize the compiled program to run faster. (Optimization takes time to complete.)") + .required(false), + ) + ) + .subcommand(App::new("repl") + .about("Launch the interactive Read Eval Print Loop (REPL)") + ) +} fn main() -> io::Result<()> { - let now = SystemTime::now(); - let argv = std::env::args().collect::>(); + let matches = build_app().get_matches(); - match argv.get(1) { - Some(filename) => { - let mut path = Path::new(filename).canonicalize().unwrap(); - - if !path.is_absolute() { - path = std::env::current_dir()?.join(path).canonicalize().unwrap(); - } - - // Step 1: build the .o file for the app - let mut file = File::open(path.clone())?; - let mut contents = String::new(); - - file.read_to_string(&mut contents)?; - - let dest_filename = path.with_extension("o"); - - gen( - Path::new(filename).to_path_buf(), - contents.as_str(), - Triple::host(), - &dest_filename, - ); - - let end_time = now.elapsed().unwrap(); - - println!( - "Finished compilation and code gen in {} ms\n", - end_time.as_millis() - ); - - let cwd = dest_filename.parent().unwrap(); - let lib_path = dest_filename.with_file_name("libroc_app.a"); - - // Step 2: turn the .o file into a .a static library - Command::new("ar") // TODO on Windows, use `link` - .args(&[ - "rcs", - lib_path.to_str().unwrap(), - dest_filename.to_str().unwrap(), - ]) - .spawn() - .expect("`ar` failed to run"); - - // Step 3: have rustc compile the host and link in the .a file - Command::new("rustc") - .args(&["-L", ".", "host.rs", "-o", "app"]) - .current_dir(cwd) - .spawn() - .expect("rustc failed to run"); - - // Step 4: Run the compiled app - Command::new(cwd.join("app")).spawn().unwrap_or_else(|err| { - panic!( - "{} failed to run: {:?}", - cwd.join("app").to_str().unwrap(), - err - ) - }); - Ok(()) - } - None => { - println!("Usage: roc FILENAME.roc"); - - Ok(()) - } + match matches.subcommand_name() { + Some("build") => build(matches.subcommand_matches("build").unwrap(), false), + Some("run") => build(matches.subcommand_matches("run").unwrap(), true), + Some("repl") => repl::main(), + _ => unreachable!(), } } -fn gen(filename: PathBuf, src: &str, target: Triple, dest_filename: &Path) { - use roc_reporting::report::{can_problem, RocDocAllocator, DEFAULT_PALETTE}; - use roc_reporting::type_error::type_problem; +pub fn build(matches: &ArgMatches, run_after_build: bool) -> io::Result<()> { + let filename = matches.value_of(FLAG_ROC_FILE).unwrap(); + let opt_level = if matches.is_present(FLAG_OPTIMIZE) { + OptLevel::Optimize + } else { + OptLevel::Normal + }; + let path = Path::new(filename); + let src_dir = path.parent().unwrap().canonicalize().unwrap(); - // Build the expr + // Create the runtime + let mut rt = Builder::new() + .thread_name("roc") + .threaded_scheduler() + .enable_io() + .build() + .expect("Error spawning initial compiler thread."); // TODO make this error nicer. + + // Spawn the root task + let path = path.canonicalize().unwrap_or_else(|err| { + use ErrorKind::*; + + match err.kind() { + NotFound => { + match path.to_str() { + Some(path_str) => println!("File not found: {}", path_str), + None => println!("Malformed file path : {:?}", path), + } + + process::exit(1); + } + _ => { + todo!("TODO Gracefully handle opening {:?} - {:?}", path, err); + } + } + }); + let binary_path = rt + .block_on(build_file(src_dir, path, opt_level)) + .expect("TODO gracefully handle block_on failing"); + + if run_after_build { + // Run the compiled app + rt.block_on(async { + Command::new(binary_path) + .spawn() + .unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err)) + .await + .map_err(|_| { + todo!("gracefully handle error after `app` spawned"); + }) + }) + .expect("TODO gracefully handle block_on failing"); + } + + Ok(()) +} + +async fn build_file( + src_dir: PathBuf, + filename: PathBuf, + opt_level: OptLevel, +) -> Result { + let compilation_start = SystemTime::now(); let arena = Bump::new(); - let (loc_expr, _output, can_problems, subs, var, constraint, home, interns) = - uniq_expr_with(&arena, src, &ImMap::default()); + // Step 1: compile the app and generate the .o file + let subs_by_module = MutMap::default(); - let mut type_problems = Vec::new(); - let (content, mut subs) = infer_expr(subs, &mut type_problems, &constraint, var); + // Release builds use uniqueness optimizations + let stdlib = match opt_level { + OptLevel::Normal => roc_builtins::std::standard_stdlib(), + OptLevel::Optimize => roc_builtins::unique::uniq_stdlib(), + }; + let loaded = roc_load::file::load(&stdlib, src_dir, filename.clone(), subs_by_module).await?; + let dest_filename = filename.with_extension("o"); + gen( + &arena, + loaded, + filename, + Triple::host(), + &dest_filename, + opt_level, + ); + + let compilation_end = compilation_start.elapsed().unwrap(); + + println!( + "Finished compilation and code gen in {} ms\n", + compilation_end.as_millis() + ); + + let cwd = dest_filename.parent().unwrap(); + let lib_path = dest_filename.with_file_name("libroc_app.a"); + + // Step 2: turn the .o file into a .a static library + Command::new("ar") // TODO on Windows, use `link` + .args(&[ + "rcs", + lib_path.to_str().unwrap(), + dest_filename.to_str().unwrap(), + ]) + .spawn() + .map_err(|_| { + todo!("gracefully handle `ar` failing to spawn."); + })? + .await + .map_err(|_| { + todo!("gracefully handle error after `ar` spawned"); + })?; + + // Step 3: have rustc compile the host and link in the .a file + let binary_path = cwd.join("app"); + + Command::new("rustc") + .args(&[ + "-L", + ".", + "--crate-type", + "bin", + "host.rs", + "-o", + binary_path.as_path().to_str().unwrap(), + ]) + .current_dir(cwd) + .spawn() + .map_err(|_| { + todo!("gracefully handle `rustc` failing to spawn."); + })? + .await + .map_err(|_| { + todo!("gracefully handle error after `rustc` spawned"); + })?; + + Ok(binary_path) +} + +fn gen( + arena: &Bump, + loaded: LoadedModule, + filename: PathBuf, + target: Triple, + dest_filename: &Path, + opt_level: OptLevel, +) { + use roc_reporting::report::{can_problem, type_problem, RocDocAllocator, DEFAULT_PALETTE}; + + let src = loaded.src; + let home = loaded.module_id; let src_lines: Vec<&str> = src.split('\n').collect(); let palette = DEFAULT_PALETTE; // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&src_lines, home, &interns); + let alloc = RocDocAllocator::new(&src_lines, home, &loaded.interns); - for problem in can_problems.into_iter() { + for problem in loaded.can_problems.into_iter() { let report = can_problem(&alloc, filename.clone(), problem); let mut buf = String::new(); @@ -129,7 +247,7 @@ fn gen(filename: PathBuf, src: &str, target: Triple, dest_filename: &Path) { println!("\n{}\n", buf); } - for problem in type_problems.into_iter() { + for problem in loaded.type_problems.into_iter() { let report = type_problem(&alloc, filename.clone(), problem); let mut buf = String::new(); @@ -138,6 +256,63 @@ fn gen(filename: PathBuf, src: &str, target: Triple, dest_filename: &Path) { println!("\n{}\n", buf); } + // Look up the types and expressions of the `provided` values + + // TODO instead of hardcoding this to `main`, use the `provided` list and gen all of them. + let ident_ids = loaded.interns.all_ident_ids.get(&home).unwrap(); + let main_ident_id = *ident_ids.get_id(&"main".into()).unwrap_or_else(|| { + todo!("TODO gracefully handle the case where `main` wasn't declared in the app") + }); + let main_symbol = Symbol::new(home, main_ident_id); + let mut main_var = None; + let mut main_expr = None; + + for (symbol, var) in loaded.exposed_vars_by_symbol { + if symbol == main_symbol { + main_var = Some(var); + + break; + } + } + + // We use a loop label here so we can break all the way out of a nested + // loop inside DeclareRec if we find the expr there. + // + // https://doc.rust-lang.org/1.30.0/book/first-edition/loops.html#loop-labels + 'find_expr: for decl in loaded.declarations { + use roc_can::def::Declaration::*; + + match decl { + Declare(def) => { + if def.pattern_vars.contains_key(&main_symbol) { + main_expr = Some(def.loc_expr); + + break 'find_expr; + } + } + + DeclareRec(defs) => { + for def in defs { + if def.pattern_vars.contains_key(&main_symbol) { + main_expr = Some(def.loc_expr); + + break 'find_expr; + } + } + } + InvalidCycle(_, _) => {} + } + } + + let loc_expr = main_expr.unwrap_or_else(|| { + panic!("TODO gracefully handle the case where `main` was declared but not exposed") + }); + let mut subs = loaded.solved.into_inner(); + let content = match main_var { + Some(var) => subs.get_without_compacting(var).content, + None => todo!("TODO gracefully handle the case where `main` was declared but not exposed"), + }; + // Generate the binary let context = Context::create(); @@ -145,7 +320,7 @@ fn gen(filename: PathBuf, src: &str, target: Triple, dest_filename: &Path) { let builder = context.create_builder(); let fpm = PassManager::create(&module); - roc_gen::llvm::build::add_passes(&fpm); + roc_gen::llvm::build::add_passes(&fpm, opt_level); fpm.initialize(); @@ -160,14 +335,14 @@ fn gen(filename: PathBuf, src: &str, target: Triple, dest_filename: &Path) { let main_fn_type = basic_type_from_layout(&arena, &context, &layout, ptr_bytes).fn_type(&[], false); - let main_fn_name = "$Test.main"; + let main_fn_name = "$main"; // Compile and add all the Procs before adding main let mut env = roc_gen::llvm::build::Env { arena: &arena, builder: &builder, context: &context, - interns, + interns: loaded.interns, module: arena.alloc(module), ptr_bytes, }; @@ -271,12 +446,16 @@ fn gen(filename: PathBuf, src: &str, target: Triple, dest_filename: &Path) { "x86-64" } - Architecture::Arm(_) => { + Architecture::Arm(_) if cfg!(feature = "target-arm") => { + // NOTE: why not enable arm and wasm by default? + // + // We had some trouble getting them to link properly. This may be resolved in the + // future, or maybe it was just some weird configuration on one machine. Target::initialize_arm(&InitializationConfig::default()); "arm" } - Architecture::Wasm32 => { + Architecture::Wasm32 if cfg!(feature = "target-webassembly") => { Target::initialize_webassembly(&InitializationConfig::default()); "wasm32" diff --git a/cli/src/helpers.rs b/cli/src/repl.rs similarity index 56% rename from cli/src/helpers.rs rename to cli/src/repl.rs index 37f2cec34c..3389fb0193 100644 --- a/cli/src/helpers.rs +++ b/cli/src/repl.rs @@ -1,28 +1,278 @@ use bumpalo::Bump; +use inkwell::context::Context; +use inkwell::execution_engine::JitFunction; +use inkwell::passes::PassManager; +use inkwell::types::BasicType; +use inkwell::OptimizationLevel; use roc_builtins::unique::uniq_stdlib; use roc_can::constraint::Constraint; use roc_can::env::Env; use roc_can::expected::Expected; -use roc_can::expr::{canonicalize_expr, Expr, Output}; +use roc_can::expr::{canonicalize_expr, Output}; use roc_can::operator; use roc_can::scope::Scope; use roc_collections::all::{ImMap, ImSet, MutMap, SendMap, SendSet}; use roc_constrain::expr::constrain_expr; use roc_constrain::module::{constrain_imported_values, load_builtin_aliases, Import}; +use roc_gen::llvm::build::{build_proc, build_proc_header, OptLevel}; +use roc_gen::llvm::convert::basic_type_from_layout; use roc_module::ident::Ident; use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds, Symbol}; +use roc_mono::expr::Procs; +use roc_mono::layout::Layout; use roc_parse::ast::{self, Attempting}; use roc_parse::blankspace::space0_before; use roc_parse::parser::{loc, Fail, Parser, State}; use roc_problem::can::Problem; use roc_region::all::{Located, Region}; use roc_solve::solve; +use roc_types::pretty_print::{content_to_string, name_all_type_vars}; use roc_types::subs::{Content, Subs, VarStore, Variable}; use roc_types::types::Type; use std::hash::Hash; +use std::io::{self, Write}; +use std::path::PathBuf; +use target_lexicon::Triple; -pub fn test_home() -> ModuleId { - ModuleIds::default().get_or_insert(&"Test".into()) +pub fn main() -> io::Result<()> { + use std::io::BufRead; + + println!( + "\n The rockin’ \u{001b}[36mroc repl\u{001b}[0m\n\u{001b}[35m────────────────────────\u{001b}[0m\n\n{}", + WELCOME_MESSAGE + ); + + // Loop + + loop { + print!("\n\u{001b}[36m▶\u{001b}[0m "); + + io::stdout().flush().unwrap(); + + let stdin = io::stdin(); + let line = stdin + .lock() + .lines() + .next() + .expect("there was no next line") + .expect("the line could not be read"); + + match line.trim() { + ":help" => { + println!("Use :exit to exit."); + } + "" => { + println!("\n{}", WELCOME_MESSAGE); + } + ":exit" => { + break; + } + line => { + let (answer, answer_type) = gen(line, Triple::host(), OptLevel::Normal); + + println!("\n{} \u{001b}[35m:\u{001b}[0m {}", answer, answer_type); + } + } + } + + Ok(()) +} + +const WELCOME_MESSAGE: &str = + "Enter an expression, or :help for a list of commands, or :exit to exit."; + +pub fn repl_home() -> ModuleId { + ModuleIds::default().get_or_insert(&"REPL".into()) +} + +pub fn gen(src: &str, target: Triple, opt_level: OptLevel) -> (String, String) { + use roc_reporting::report::{can_problem, type_problem, RocDocAllocator, DEFAULT_PALETTE}; + + // Look up the types and expressions of the `provided` values + let ptr_bytes = target.pointer_width().unwrap().bytes() as u32; + let arena = Bump::new(); + let CanExprOut { + loc_expr, + var_store, + var, + constraint, + home, + interns, + problems: can_problems, + .. + } = can_expr(src); + let subs = Subs::new(var_store.into()); + let mut type_problems = Vec::new(); + let (content, mut subs) = infer_expr(subs, &mut type_problems, &constraint, var); + + // Report problems + let src_lines: Vec<&str> = src.split('\n').collect(); + let palette = DEFAULT_PALETTE; + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + + // Used for reporting where an error came from. + // + // TODO: maybe Reporting should have this be an Option? + let path = PathBuf::new(); + + for problem in can_problems.into_iter() { + let report = can_problem(&alloc, path.clone(), problem); + let mut buf = String::new(); + + report.render_color_terminal(&mut buf, &alloc, &palette); + + println!("\n{}\n", buf); + } + + for problem in type_problems.into_iter() { + let report = type_problem(&alloc, path.clone(), problem); + let mut buf = String::new(); + + report.render_color_terminal(&mut buf, &alloc, &palette); + + println!("\n{}\n", buf); + } + + let context = Context::create(); + let module = roc_gen::llvm::build::module_from_builtins(&context, "app"); + let builder = context.create_builder(); + let fpm = PassManager::create(&module); + + roc_gen::llvm::build::add_passes(&fpm, opt_level); + + fpm.initialize(); + + // pretty-print the expr type string for later. + name_all_type_vars(var, &mut subs); + + let expr_type_str = content_to_string(content.clone(), &subs, home, &interns); + + // Compute main_fn_type before moving subs to Env + let layout = Layout::from_content(&arena, content, &subs, ptr_bytes).unwrap_or_else(|err| { + panic!( + "Code gen error in test: could not convert to layout. Err was {:?} and Subs were {:?}", + err, subs + ) + }); + let execution_engine = module + .create_jit_execution_engine(OptimizationLevel::None) + .expect("Error creating JIT execution engine for test"); + + let main_fn_type = + basic_type_from_layout(&arena, &context, &layout, ptr_bytes).fn_type(&[], false); + let main_fn_name = "$Test.main"; + + // Compile and add all the Procs before adding main + let mut env = roc_gen::llvm::build::Env { + arena: &arena, + builder: &builder, + context: &context, + interns, + module: arena.alloc(module), + ptr_bytes, + }; + let mut procs = Procs::default(); + let mut ident_ids = env.interns.all_ident_ids.remove(&home).unwrap(); + + // Populate Procs and get the low-level Expr from the canonical Expr + let mut mono_problems = Vec::new(); + let main_body = roc_mono::expr::Expr::new( + &arena, + &mut subs, + &mut mono_problems, + loc_expr.value, + &mut procs, + home, + &mut ident_ids, + ptr_bytes, + ); + + // Put this module's ident_ids back in the interns, so we can use them in Env. + env.interns.all_ident_ids.insert(home, ident_ids); + + let mut headers = Vec::with_capacity(procs.len()); + + // Add all the Proc headers to the module. + // We have to do this in a separate pass first, + // because their bodies may reference each other. + for (symbol, opt_proc) in procs.as_map().into_iter() { + if let Some(proc) = opt_proc { + let (fn_val, arg_basic_types) = build_proc_header(&env, symbol, &proc); + + headers.push((proc, fn_val, arg_basic_types)); + } + } + + // Build each proc using its header info. + for (proc, fn_val, arg_basic_types) in headers { + // NOTE: This is here to be uncommented in case verification fails. + // (This approach means we don't have to defensively clone name here.) + // + // println!("\n\nBuilding and then verifying function {}\n\n", name); + build_proc(&env, proc, &procs, fn_val, arg_basic_types); + + if fn_val.verify(true) { + fpm.run_on(&fn_val); + } else { + // NOTE: If this fails, uncomment the above println to debug. + panic!( + "Non-main function failed LLVM verification. Uncomment the above println to debug!" + ); + } + } + + // Add main to the module. + let main_fn = env.module.add_function(main_fn_name, main_fn_type, None); + let cc = + roc_gen::llvm::build::get_call_conventions(target.default_calling_convention().unwrap()); + + main_fn.set_call_conventions(cc); + + // Add main's body + let basic_block = context.append_basic_block(main_fn, "entry"); + + builder.position_at_end(basic_block); + + let ret = roc_gen::llvm::build::build_expr( + &env, + &ImMap::default(), + main_fn, + &main_body, + &Procs::default(), + ); + + builder.build_return(Some(&ret)); + + // Uncomment this to see the module's un-optimized LLVM instruction output: + // env.module.print_to_stderr(); + + if main_fn.verify(true) { + fpm.run_on(&main_fn); + } else { + panic!("Function {} failed LLVM verification.", main_fn_name); + } + + // Verify the module + if let Err(errors) = env.module.verify() { + panic!("Errors defining module: {:?}", errors); + } + + // Uncomment this to see the module's optimized LLVM instruction output: + // env.module.print_to_stderr(); + + unsafe { + let main: JitFunction< + unsafe extern "C" fn() -> i64, /* TODO have this return Str, and in the generated code make sure to call the appropriate string conversion function on the return val based on its type! */ + > = execution_engine + .get_function(main_fn_name) + .ok() + .ok_or(format!("Unable to JIT compile `{}`", main_fn_name)) + .expect("errored"); + + (format!("{}", main.call()), expr_type_str) + } } pub fn infer_expr( @@ -57,13 +307,13 @@ pub fn parse_loc_with<'a>(arena: &'a Bump, input: &'a str) -> Result CanExprOut { - can_expr_with(&Bump::new(), test_home(), expr_str) + can_expr_with(&Bump::new(), repl_home(), expr_str) } pub fn uniq_expr( expr_str: &str, ) -> ( - Located, + Located, Output, Vec, Subs, @@ -82,7 +332,7 @@ pub fn uniq_expr_with( expr_str: &str, declared_idents: &ImMap, ) -> ( - Located, + Located, Output, Vec, Subs, @@ -91,7 +341,7 @@ pub fn uniq_expr_with( ModuleId, Interns, ) { - let home = test_home(); + let home = repl_home(); let CanExprOut { loc_expr, output, @@ -145,7 +395,7 @@ pub fn uniq_expr_with( } pub struct CanExprOut { - pub loc_expr: Located, + pub loc_expr: Located, pub output: Output, pub problems: Vec, pub home: ModuleId, diff --git a/cli/tests/cli_run.rs b/cli/tests/cli_run.rs new file mode 100644 index 0000000000..d9fd90c41e --- /dev/null +++ b/cli/tests/cli_run.rs @@ -0,0 +1,104 @@ +#[macro_use] +extern crate pretty_assertions; + +extern crate bumpalo; +extern crate inlinable_string; +extern crate roc_collections; +extern crate roc_load; +extern crate roc_module; + +#[cfg(test)] +mod cli_run { + use std::env; + use std::path::PathBuf; + use std::process::{Command, ExitStatus}; + + // HELPERS + + pub struct Out { + pub stdout: String, + pub stderr: String, + pub status: ExitStatus, + } + + pub fn path_to_roc_binary() -> PathBuf { + // Adapted from https://github.com/volta-cli/volta/blob/cefdf7436a15af3ce3a38b8fe53bb0cfdb37d3dd/tests/acceptance/support/sandbox.rs#L680 - BSD-2-Clause licensed + let mut path = env::var_os("CARGO_BIN_PATH") + .map(PathBuf::from) + .or_else(|| { + env::current_exe().ok().map(|mut path| { + path.pop(); + if path.ends_with("deps") { path.pop(); + } + path + }) + }) + .unwrap_or_else(|| panic!("CARGO_BIN_PATH wasn't set, and couldn't be inferred from context. Can't run CLI tests.")); + + path.push("roc"); + + path + } + + pub fn run_roc(args: &[&str]) -> Out { + let mut cmd = Command::new(path_to_roc_binary()); + + for arg in args { + cmd.arg(arg); + } + + let output = cmd + .output() + .expect("failed to execute compiled `roc` binary in CLI test"); + + Out { + stdout: String::from_utf8(output.stdout).unwrap(), + stderr: String::from_utf8(output.stderr).unwrap(), + status: output.status, + } + } + + pub fn example_dir(dir_name: &str) -> PathBuf { + let mut path = env::current_exe().ok().unwrap(); + + // Get rid of the filename in target/debug/deps/cli_run-99c65e4e9a1fbd06 + path.pop(); + + // If we're in deps/ get rid of deps/ in target/debug/deps/ + if path.ends_with("deps") { + path.pop(); + } + + // Get rid of target/debug/ so we're back at the project root + path.pop(); + path.pop(); + + // Descend into examples/{dir_name} + path.push("examples"); + path.push(dir_name); + + path + } + + pub fn example_file(dir_name: &str, file_name: &str) -> PathBuf { + let mut path = example_dir(dir_name); + + path.push(file_name); + + path + } + + // TESTS + + #[test] + fn run_hello_world() { + let out = run_roc(&[ + "run", + example_file("hello-world", "Hello.roc").to_str().unwrap(), + ]); + + assert_eq!(&out.stderr, ""); + assert!(&out.stdout.ends_with("Hello, World!\n")); + assert!(out.status.success()); + } +} diff --git a/compiler/builtins/Cargo.toml b/compiler/builtins/Cargo.toml index 1abfe286de..72f6b03686 100644 --- a/compiler/builtins/Cargo.toml +++ b/compiler/builtins/Cargo.toml @@ -12,7 +12,7 @@ roc_types = { path = "../types" } roc_can = { path = "../can" } [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/can/Cargo.toml b/compiler/can/Cargo.toml index 7addf23998..5220723c30 100644 --- a/compiler/can/Cargo.toml +++ b/compiler/can/Cargo.toml @@ -18,7 +18,7 @@ bumpalo = { version = "3.2", features = ["collections"] } inlinable_string = "0.1.0" [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/can/src/annotation.rs b/compiler/can/src/annotation.rs index c70e90fcd6..a5a1cf6d4f 100644 --- a/compiler/can/src/annotation.rs +++ b/compiler/can/src/annotation.rs @@ -1,6 +1,6 @@ use crate::env::Env; use crate::scope::Scope; -use roc_collections::all::{MutSet, SendMap}; +use roc_collections::all::{MutMap, MutSet, SendMap}; use roc_module::ident::Ident; use roc_module::ident::{Lowercase, TagName}; use roc_module::symbol::Symbol; @@ -292,9 +292,10 @@ fn can_annotation_help( Record { fields, ext } => { let mut field_types = SendMap::default(); + let mut seen = MutMap::default(); for field in fields.iter() { - can_assigned_field( + let opt_field_name = can_assigned_field( env, &field.value, region, @@ -305,6 +306,17 @@ fn can_annotation_help( &mut field_types, references, ); + + if let Some(added) = opt_field_name { + if let Some(replaced_region) = seen.insert(added.clone(), field.region) { + env.problem(roc_problem::can::Problem::DuplicateRecordFieldType { + field_name: added.clone(), + field_region: field.region, + record_region: region, + replaced_region, + }); + } + } } let ext_type = match ext { @@ -325,9 +337,10 @@ fn can_annotation_help( } TagUnion { tags, ext } => { let mut tag_types = Vec::with_capacity(tags.len()); + let mut seen = MutMap::default(); for tag in tags.iter() { - can_tag( + let opt_tag_name = can_tag( env, &tag.value, region, @@ -338,6 +351,17 @@ fn can_annotation_help( &mut tag_types, references, ); + + if let Some(added) = opt_tag_name { + if let Some(replaced_region) = seen.insert(added.clone(), tag.region) { + env.problem(roc_problem::can::Problem::DuplicateTag { + tag_name: added.clone(), + tag_region: tag.region, + tag_union_region: region, + replaced_region, + }); + } + } } let ext_type = match ext { @@ -388,7 +412,7 @@ fn can_assigned_field<'a>( local_aliases: &mut SendMap, field_types: &mut SendMap, references: &mut MutSet, -) { +) -> Option { use roc_parse::ast::AssignedField::*; match field { @@ -396,16 +420,18 @@ fn can_assigned_field<'a>( let field_type = can_annotation_help( env, &annotation.value, - region, + annotation.region, scope, var_store, introduced_variables, local_aliases, references, ); - let label = Lowercase::from(field_name.value); - field_types.insert(label, field_type); + let label = Lowercase::from(field_name.value); + field_types.insert(label.clone(), field_type); + + Some(label) } LabelOnly(loc_field_name) => { // Interpret { a, b } as { a : a, b : b } @@ -420,7 +446,9 @@ fn can_assigned_field<'a>( } }; - field_types.insert(field_name, field_type); + field_types.insert(field_name.clone(), field_type); + + Some(field_name) } SpaceBefore(nested, _) | SpaceAfter(nested, _) => can_assigned_field( env, @@ -433,7 +461,7 @@ fn can_assigned_field<'a>( field_types, references, ), - Malformed(_) => (), + Malformed(_) => None, } } @@ -449,7 +477,7 @@ fn can_tag<'a>( local_aliases: &mut SendMap, tag_types: &mut Vec<(TagName, Vec)>, references: &mut MutSet, -) { +) -> Option { match tag { Tag::Global { name, args } => { let name = name.value.into(); @@ -470,7 +498,10 @@ fn can_tag<'a>( arg_types.push(ann); } - tag_types.push((TagName::Global(name), arg_types)); + let tag_name = TagName::Global(name); + tag_types.push((tag_name.clone(), arg_types)); + + Some(tag_name) } Tag::Private { name, args } => { let ident_id = env.ident_ids.get_or_insert(&name.value.into()); @@ -492,7 +523,10 @@ fn can_tag<'a>( arg_types.push(ann); } - tag_types.push((TagName::Private(symbol), arg_types)); + let tag_name = TagName::Private(symbol); + tag_types.push((tag_name.clone(), arg_types)); + + Some(tag_name) } Tag::SpaceBefore(nested, _) | Tag::SpaceAfter(nested, _) => can_tag( env, @@ -505,6 +539,6 @@ fn can_tag<'a>( tag_types, references, ), - Tag::Malformed(_) => (), + Tag::Malformed(_) => None, } } diff --git a/compiler/can/src/constraint.rs b/compiler/can/src/constraint.rs index 1d39fa5aa6..0d029c3b96 100644 --- a/compiler/can/src/constraint.rs +++ b/compiler/can/src/constraint.rs @@ -32,24 +32,32 @@ impl Constraint { match self { True | SaveTheEnvironment => {} - Eq(typ, expected, _, _) => { - expected - .get_type_mut_ref() - .instantiate_aliases(aliases, var_store, introduced); - typ.instantiate_aliases(aliases, var_store, introduced); + Eq(typ, expected, _, region) => { + let expected_region = expected.get_annotation_region().unwrap_or(*region); + expected.get_type_mut_ref().instantiate_aliases( + expected_region, + aliases, + var_store, + introduced, + ); + typ.instantiate_aliases(*region, aliases, var_store, introduced); } - Lookup(_, expected, _) => { - expected - .get_type_mut_ref() - .instantiate_aliases(aliases, var_store, introduced); + Lookup(_, expected, region) => { + let expected_region = expected.get_annotation_region().unwrap_or(*region); + expected.get_type_mut_ref().instantiate_aliases( + expected_region, + aliases, + var_store, + introduced, + ); } - Pattern(_, _, typ, pexpected) => { + Pattern(region, _, typ, pexpected) => { pexpected .get_type_mut_ref() - .instantiate_aliases(aliases, var_store, introduced); - typ.instantiate_aliases(aliases, var_store, introduced); + .instantiate_aliases(*region, aliases, var_store, introduced); + typ.instantiate_aliases(*region, aliases, var_store, introduced); } And(nested) => { @@ -65,8 +73,8 @@ impl Constraint { } let mut introduced = ImSet::default(); - for Located { value: typ, .. } in letcon.def_types.iter_mut() { - typ.instantiate_aliases(&new_aliases, var_store, &mut introduced); + for Located { region, value: typ } in letcon.def_types.iter_mut() { + typ.instantiate_aliases(*region, &new_aliases, var_store, &mut introduced); } letcon.defs_constraint.instantiate_aliases_help( diff --git a/compiler/can/src/def.rs b/compiler/can/src/def.rs index 0caf691663..d4835a78dc 100644 --- a/compiler/can/src/def.rs +++ b/compiler/can/src/def.rs @@ -201,6 +201,7 @@ pub fn canonicalize_defs<'a>( let mut can_vars: Vec> = Vec::with_capacity(vars.len()); + let mut is_phantom = false; for loc_lowercase in vars { if let Some(var) = can_ann .introduced_variables @@ -212,12 +213,31 @@ pub fn canonicalize_defs<'a>( region: loc_lowercase.region, }); } else { - panic!("TODO handle phantom type variables, they are not allowed!\nThe {:?} variable in the definition of {:?} gives trouble", loc_lowercase, symbol); + is_phantom = true; + + env.problems.push(Problem::PhantomTypeArgument { + alias: symbol, + variable_region: loc_lowercase.region, + variable_name: loc_lowercase.value.clone(), + }); } } + if is_phantom { + // Bail out + continue; + } + if can_ann.typ.contains_symbol(symbol) { - make_tag_union_recursive(symbol, &mut can_ann.typ, var_store); + make_tag_union_recursive( + env, + symbol, + name.region, + vec![], + &mut can_ann.typ, + var_store, + &mut false, + ); } let alias = roc_types::types::Alias { @@ -231,7 +251,7 @@ pub fn canonicalize_defs<'a>( } } - correct_mutual_recursive_type_alias(&mut aliases, &var_store); + correct_mutual_recursive_type_alias(env, &mut aliases, &var_store); // Now that we have the scope completely assembled, and shadowing resolved, // we're ready to canonicalize any body exprs. @@ -836,7 +856,11 @@ fn canonicalize_pending_def<'a>( region: loc_lowercase.region, }); } else { - panic!("TODO handle phantom type variables, they are not allowed!"); + env.problems.push(Problem::PhantomTypeArgument { + alias: symbol, + variable_region: loc_lowercase.region, + variable_name: loc_lowercase.value.clone(), + }); } } @@ -852,7 +876,9 @@ fn canonicalize_pending_def<'a>( scope.add_alias(symbol, name.region, can_vars, rec_type_union); } else { - panic!("recursion in type alias that is not behind a Tag"); + env.problems + .push(Problem::CyclicAlias(symbol, name.region, vec![])); + return output; } } @@ -1385,7 +1411,11 @@ fn pending_typed_body<'a>( } /// Make aliases recursive -fn correct_mutual_recursive_type_alias(aliases: &mut SendMap, var_store: &VarStore) { +fn correct_mutual_recursive_type_alias<'a>( + env: &mut Env<'a>, + aliases: &mut SendMap, + var_store: &VarStore, +) { let mut symbols_introduced = ImSet::default(); for (key, _) in aliases.iter() { @@ -1434,11 +1464,17 @@ fn correct_mutual_recursive_type_alias(aliases: &mut SendMap, var &mutually_recursive_symbols, all_successors_without_self, ) { + // make sure we report only one error for the cycle, not an error for every + // alias in the cycle. + let mut can_still_report_error = true; + // TODO use itertools to be more efficient here for rec in &cycle { let mut to_instantiate = ImMap::default(); + let mut others = Vec::with_capacity(cycle.len() - 1); for other in &cycle { if rec != other { + others.push(*other); if let Some(alias) = originals.get(other) { to_instantiate.insert(*other, alias.clone()); } @@ -1447,11 +1483,20 @@ fn correct_mutual_recursive_type_alias(aliases: &mut SendMap, var if let Some(alias) = aliases.get_mut(rec) { alias.typ.instantiate_aliases( + alias.region, &to_instantiate, var_store, &mut ImSet::default(), ); - make_tag_union_recursive(*rec, &mut alias.typ, var_store); + make_tag_union_recursive( + env, + *rec, + alias.region, + others, + &mut alias.typ, + var_store, + &mut can_still_report_error, + ); } } } @@ -1459,14 +1504,41 @@ fn correct_mutual_recursive_type_alias(aliases: &mut SendMap, var } } -fn make_tag_union_recursive(symbol: Symbol, typ: &mut Type, var_store: &VarStore) { +fn make_tag_union_recursive<'a>( + env: &mut Env<'a>, + symbol: Symbol, + region: Region, + others: Vec, + typ: &mut Type, + var_store: &VarStore, + can_report_error: &mut bool, +) { match typ { Type::TagUnion(tags, ext) => { let rec_var = var_store.fresh(); *typ = Type::RecursiveTagUnion(rec_var, tags.to_vec(), ext.clone()); typ.substitute_alias(symbol, &Type::Variable(rec_var)); } - Type::Alias(_, _, actual) => make_tag_union_recursive(symbol, actual, var_store), - _ => panic!("recursion in type alias is not behind a Tag"), + Type::Alias(_, _, actual) => make_tag_union_recursive( + env, + symbol, + region, + others, + actual, + var_store, + can_report_error, + ), + _ => { + let problem = roc_types::types::Problem::CyclicAlias(symbol, region, others.clone()); + *typ = Type::Erroneous(problem); + + // ensure cyclic error is only reported for one element of the cycle + if *can_report_error { + *can_report_error = false; + + let problem = Problem::CyclicAlias(symbol, region, others); + env.problems.push(problem); + } + } } } diff --git a/compiler/can/src/expected.rs b/compiler/can/src/expected.rs index b7ef500d9f..4075624dcc 100644 --- a/compiler/can/src/expected.rs +++ b/compiler/can/src/expected.rs @@ -71,6 +71,15 @@ impl Expected { } } + pub fn get_annotation_region(&self) -> Option { + match self { + Expected::FromAnnotation(_, _, AnnotationSource::TypedBody { region }, _) => { + Some(*region) + } + _ => None, + } + } + pub fn replace(self, new: U) -> Expected { match self { Expected::NoExpectation(_val) => Expected::NoExpectation(new), diff --git a/compiler/can/src/expr.rs b/compiler/can/src/expr.rs index 6889281ee7..1289a61730 100644 --- a/compiler/can/src/expr.rs +++ b/compiler/can/src/expr.rs @@ -193,7 +193,8 @@ pub fn canonicalize_expr<'a>( let (can_update, update_out) = canonicalize_expr(env, var_store, scope, loc_update.region, &loc_update.value); if let Var(symbol) = &can_update.value { - let (can_fields, mut output) = canonicalize_fields(env, var_store, scope, fields); + let (can_fields, mut output) = + canonicalize_fields(env, var_store, scope, region, fields); output.references = output.references.union(update_out.references); @@ -219,7 +220,8 @@ pub fn canonicalize_expr<'a>( if fields.is_empty() { (EmptyRecord, Output::default()) } else { - let (can_fields, output) = canonicalize_fields(env, var_store, scope, fields); + let (can_fields, output) = + canonicalize_fields(env, var_store, scope, region, fields); ( Record { @@ -885,6 +887,7 @@ fn canonicalize_fields<'a>( env: &mut Env<'a>, var_store: &VarStore, scope: &mut Scope, + region: Region, fields: &'a [Located>>], ) -> (SendMap, Output) { let mut can_fields = SendMap::default(); @@ -900,7 +903,16 @@ fn canonicalize_fields<'a>( loc_expr: Box::new(field_expr), }; - can_fields.insert(label, field); + let replaced = can_fields.insert(label.clone(), field); + + if let Some(old) = replaced { + env.problems.push(Problem::DuplicateRecordFieldValue { + field_name: label, + field_region: loc_field.region, + record_region: region, + replaced_region: old.region, + }); + } output.references = output.references.union(field_out.references); } diff --git a/compiler/constrain/Cargo.toml b/compiler/constrain/Cargo.toml index 480097648c..c0efe16499 100644 --- a/compiler/constrain/Cargo.toml +++ b/compiler/constrain/Cargo.toml @@ -15,7 +15,7 @@ roc_builtins = { path = "../builtins" } roc_uniq = { path = "../uniq" } [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/constrain/src/module.rs b/compiler/constrain/src/module.rs index 5b6991bc9e..863abff1d6 100644 --- a/compiler/constrain/src/module.rs +++ b/compiler/constrain/src/module.rs @@ -277,9 +277,7 @@ fn to_type(solved_type: &SolvedType, free_vars: &mut FreeVars, var_store: &VarSt Type::Alias(*symbol, type_variables, Box::new(actual)) } - Error => { - panic!("TODO convert from SolvedType::Error to Type somehow"); - } + Error => Type::Erroneous(roc_types::types::Problem::SolvedTypeError), Erroneous(problem) => Type::Erroneous(problem.clone()), } } diff --git a/compiler/fmt/Cargo.toml b/compiler/fmt/Cargo.toml index 4399766724..7f38cd0c71 100644 --- a/compiler/fmt/Cargo.toml +++ b/compiler/fmt/Cargo.toml @@ -17,7 +17,7 @@ bumpalo = { version = "3.2", features = ["collections"] } inlinable_string = "0.1.0" [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/gen/Cargo.toml b/compiler/gen/Cargo.toml index 0b74a834a2..e7fcc4d47e 100644 --- a/compiler/gen/Cargo.toml +++ b/compiler/gen/Cargo.toml @@ -43,7 +43,7 @@ target-lexicon = "0.10" [dev-dependencies] roc_can = { path = "../can" } roc_parse = { path = "../parse" } -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/gen/src/llvm/build.rs b/compiler/gen/src/llvm/build.rs index 7694951747..6a57b21537 100644 --- a/compiler/gen/src/llvm/build.rs +++ b/compiler/gen/src/llvm/build.rs @@ -4,11 +4,11 @@ use inkwell::builder::Builder; use inkwell::context::Context; use inkwell::memory_buffer::MemoryBuffer; use inkwell::module::{Linkage, Module}; -use inkwell::passes::PassManager; +use inkwell::passes::{PassManager, PassManagerBuilder}; use inkwell::types::{BasicTypeEnum, IntType, StructType}; use inkwell::values::BasicValueEnum::{self, *}; use inkwell::values::{FunctionValue, IntValue, PointerValue, StructValue}; -use inkwell::{FloatPredicate, IntPredicate}; +use inkwell::{FloatPredicate, IntPredicate, OptimizationLevel}; use crate::llvm::convert::{ basic_type_from_layout, collection_wrapper, empty_collection, get_fn_type, ptr_int, @@ -31,6 +31,11 @@ const PRINT_FN_VERIFICATION_OUTPUT: bool = false; // TODO: experiment with different internal calling conventions, e.g. "fast" const DEFAULT_CALLING_CONVENTION: u32 = 0; +pub enum OptLevel { + Normal, + Optimize, +} + type Scope<'a, 'ctx> = ImMap, PointerValue<'ctx>)>; pub struct Env<'a, 'ctx, 'env> { @@ -56,23 +61,41 @@ pub fn module_from_builtins<'ctx>(ctx: &'ctx Context, module_name: &str) -> Modu .unwrap_or_else(|err| panic!("Unable to import builtins bitcode. LLVM error: {:?}", err)) } -pub fn add_passes(fpm: &PassManager>) { +pub fn add_passes(fpm: &PassManager>, opt_level: OptLevel) { // tail-call elimination is always on fpm.add_instruction_combining_pass(); fpm.add_tail_call_elimination_pass(); + let pmb = PassManagerBuilder::create(); + // Enable more optimizations when running cargo test --release - if !cfg!(debug_assertions) { - fpm.add_reassociate_pass(); - fpm.add_basic_alias_analysis_pass(); - fpm.add_promote_memory_to_register_pass(); - fpm.add_cfg_simplification_pass(); - fpm.add_gvn_pass(); - // TODO figure out why enabling any of these (even alone) causes LLVM to segfault - // fpm.add_strip_dead_prototypes_pass(); - // fpm.add_dead_arg_elimination_pass(); - // fpm.add_function_inlining_pass(); + match opt_level { + OptLevel::Normal => { + pmb.set_optimization_level(OptimizationLevel::None); + } + OptLevel::Optimize => { + // Default is O2, Aggressive is O3 + // + // See https://llvm.org/doxygen/CodeGen_8h_source.html + pmb.set_optimization_level(OptimizationLevel::Aggressive); + + // TODO figure out how enabling these individually differs from + // the broad "aggressive optimizations" setting. + + // fpm.add_reassociate_pass(); + // fpm.add_basic_alias_analysis_pass(); + // fpm.add_promote_memory_to_register_pass(); + // fpm.add_cfg_simplification_pass(); + // fpm.add_gvn_pass(); + // TODO figure out why enabling any of these (even alone) causes LLVM to segfault + // fpm.add_strip_dead_prototypes_pass(); + // fpm.add_dead_arg_elimination_pass(); + // fpm.add_function_inlining_pass(); + // pmb.set_inliner_with_threshold(4); + } } + + pmb.populate_function_pass_manager(&fpm); } #[allow(clippy::cognitive_complexity)] @@ -1006,6 +1029,17 @@ fn call_with_args<'a, 'ctx, 'env>( BasicValueEnum::FloatValue(float_val) } + Symbol::FLOAT_DIV => { + debug_assert!(args.len() == 2); + + let float_val = env.builder.build_float_div( + args[0].0.into_float_value(), + args[1].0.into_float_value(), + "div_f64", + ); + + BasicValueEnum::FloatValue(float_val) + } Symbol::NUM_MUL => { debug_assert!(args.len() == 2); diff --git a/compiler/gen/tests/gen_builtins.rs b/compiler/gen/tests/gen_builtins.rs index 174efe8e47..095f27c693 100644 --- a/compiler/gen/tests/gen_builtins.rs +++ b/compiler/gen/tests/gen_builtins.rs @@ -84,6 +84,19 @@ mod gen_builtins { ); } + #[test] + fn gen_div_f64() { + assert_evals_to!( + indoc!( + r#" + 48 / 2 + "# + ), + 24.0, + f64 + ); + } + #[test] fn gen_add_i64() { assert_evals_to!( @@ -148,6 +161,20 @@ mod gen_builtins { i64 ); } + + #[test] + fn gen_order_of_arithmetic_ops_complex_float() { + assert_evals_to!( + indoc!( + r#" + 48 / 2 + 3 + "# + ), + 27.0, + f64 + ); + } + #[test] fn if_guard_bind_variable() { assert_evals_to!( diff --git a/compiler/gen/tests/helpers/eval.rs b/compiler/gen/tests/helpers/eval.rs index 8b5fefffd9..7e30f17c5c 100644 --- a/compiler/gen/tests/helpers/eval.rs +++ b/compiler/gen/tests/helpers/eval.rs @@ -12,9 +12,14 @@ macro_rules! assert_llvm_evals_to { let context = Context::create(); let module = roc_gen::llvm::build::module_from_builtins(&context, "app"); let builder = context.create_builder(); - let fpm = inkwell::passes::PassManager::create(&module); + let opt_level = if cfg!(debug_assertions) { + roc_gen::llvm::build::OptLevel::Normal + } else { + roc_gen::llvm::build::OptLevel::Optimize + }; + let fpm = PassManager::create(&module); - roc_gen::llvm::build::add_passes(&fpm); + roc_gen::llvm::build::add_passes(&fpm, opt_level); fpm.initialize(); @@ -142,12 +147,19 @@ macro_rules! assert_opt_evals_to { let mut unify_problems = Vec::new(); let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var); + assert_eq!(unify_problems, Vec::new(), "Encountered one or more type mismatches: {:?}", unify_problems); + let context = Context::create(); let module = roc_gen::llvm::build::module_from_builtins(&context, "app"); let builder = context.create_builder(); + let opt_level = if cfg!(debug_assertions) { + roc_gen::llvm::build::OptLevel::Normal + } else { + roc_gen::llvm::build::OptLevel::Optimize + }; let fpm = PassManager::create(&module); - roc_gen::llvm::build::add_passes(&fpm); + roc_gen::llvm::build::add_passes(&fpm, opt_level); fpm.initialize(); @@ -271,12 +283,19 @@ macro_rules! emit_expr { let mut unify_problems = Vec::new(); let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var); + assert_eq!(unify_problems, Vec::new(), "Encountered one or more type mismatches: {:?}", unify_problems); + let context = Context::create(); let module = context.create_module("app"); let builder = context.create_builder(); + let opt_level = if cfg!(debug_assertions) { + roc_gen::llvm::build::OptLevel::Normal + } else { + roc_gen::llvm::build::OptLevel::Optimize + }; let fpm = PassManager::create(&module); - roc_gen::llvm::build::add_passes(&fpm); + roc_gen::llvm::build::add_passes(&fpm, opt_level); fpm.initialize(); diff --git a/compiler/load/Cargo.toml b/compiler/load/Cargo.toml index 2655dff932..af3e4e3b0f 100644 --- a/compiler/load/Cargo.toml +++ b/compiler/load/Cargo.toml @@ -21,7 +21,7 @@ inlinable_string = "0.1.0" tokio = { version = "0.2", features = ["blocking", "fs", "sync", "rt-threaded"] } [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/load/src/file.rs b/compiler/load/src/file.rs index b5c8bf0eba..6e95fec2dd 100644 --- a/compiler/load/src/file.rs +++ b/compiler/load/src/file.rs @@ -12,7 +12,7 @@ use roc_constrain::module::{ }; use roc_module::ident::{Ident, Lowercase, ModuleName}; use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds, Symbol}; -use roc_parse::ast::{self, Attempting, ExposesEntry, ImportsEntry, InterfaceHeader}; +use roc_parse::ast::{self, Attempting, ExposesEntry, ImportsEntry}; use roc_parse::module::module_defs; use roc_parse::parser::{Fail, Parser, State}; use roc_region::all::{Located, Region}; @@ -44,6 +44,7 @@ pub struct Module { pub aliases: MutMap, pub rigid_variables: MutMap, pub imported_modules: MutSet, + pub src: Box, } #[derive(Debug)] @@ -54,6 +55,8 @@ pub struct LoadedModule { pub can_problems: Vec, pub type_problems: Vec, pub declarations: Vec, + pub exposed_vars_by_symbol: Vec<(Symbol, Variable)>, + pub src: Box, } #[derive(Debug, Clone)] @@ -89,10 +92,12 @@ enum Msg { var_store: VarStore, }, Solved { + src: Box, module_id: ModuleId, solved_types: MutMap, aliases: MutMap, subs: Arc>, + exposed_vars_by_symbol: Vec<(Symbol, Variable)>, problems: Vec, }, } @@ -108,6 +113,7 @@ pub enum LoadingProblem { fail: Fail, }, MsgChannelDied, + TriedToImportAppModule, } enum MaybeShared<'a, 'b, A, B> { @@ -391,8 +397,9 @@ pub async fn load<'a>( solved_types, subs, problems, + exposed_vars_by_symbol, aliases, - .. + src, } => { type_problems.extend(problems); @@ -427,6 +434,8 @@ pub async fn load<'a>( can_problems, type_problems, declarations, + exposed_vars_by_symbol, + src, }); } else { // This was a dependency. Write it down and keep processing messages. @@ -515,13 +524,36 @@ fn load_filename( #[allow(clippy::let_and_return)] let answer = match roc_parse::module::header().parse(&arena, state) { Ok((ast::Module::Interface { header }, state)) => { - let module_id = send_interface_header(header, state, module_ids, msg_tx); + let module_id = send_header( + header.name, + header.exposes.into_bump_slice(), + header.imports.into_bump_slice(), + state, + module_ids, + msg_tx, + ); Ok(module_id) } - Ok((ast::Module::App { .. }, _)) => { - panic!("TODO finish loading an App module"); - } + Ok((ast::Module::App { header }, state)) => match module_ids { + MaybeShared::Shared(_, _) => { + // If this is Shared, it means we're trying to import + // an app module which is not the root. Not alllowed! + Err(LoadingProblem::TriedToImportAppModule) + } + unique_modules @ MaybeShared::Unique(_, _) => { + let module_id = send_header( + header.name, + header.provides.into_bump_slice(), + header.imports.into_bump_slice(), + state, + unique_modules, + msg_tx, + ); + + Ok(module_id) + } + }, Err((fail, _)) => Err(LoadingProblem::ParsingFailed { filename, fail }), }; @@ -534,36 +566,37 @@ fn load_filename( } } -fn send_interface_header<'a>( - header: InterfaceHeader<'a>, +fn send_header<'a>( + name: Located>, + exposes: &'a [Located>], + imports: &'a [Located>], state: State<'a>, shared_modules: SharedModules<'_, '_>, msg_tx: MsgSender, ) -> ModuleId { use MaybeShared::*; - let declared_name: ModuleName = header.name.value.as_str().into(); + let declared_name: ModuleName = name.value.as_str().into(); // TODO check to see if declared_name is consistent with filename. // If it isn't, report a problem! - let mut imports: Vec<(ModuleName, Vec, Region)> = - Vec::with_capacity(header.imports.len()); + let mut imported: Vec<(ModuleName, Vec, Region)> = Vec::with_capacity(imports.len()); let mut imported_modules: MutSet = MutSet::default(); let mut scope_size = 0; - for loc_entry in header.imports { + for loc_entry in imports { let (module_name, exposed) = exposed_from_import(&loc_entry.value); scope_size += exposed.len(); - imports.push((module_name, exposed, loc_entry.region)); + imported.push((module_name, exposed, loc_entry.region)); } - let num_exposes = header.exposes.len(); + let num_exposes = exposes.len(); let mut deps_by_name: MutMap = HashMap::with_capacity_and_hasher(num_exposes, default_hasher()); - let mut exposes: Vec = Vec::with_capacity(num_exposes); + let mut exposed: Vec = Vec::with_capacity(num_exposes); // Make sure the module_ids has ModuleIds for all our deps, // then record those ModuleIds in can_module_ids for later. @@ -592,7 +625,7 @@ fn send_interface_header<'a>( // For each of our imports, add an entry to deps_by_name // // e.g. for `imports [ Foo.{ bar } ]`, add `Foo` to deps_by_name - for (module_name, exposed, region) in imports.into_iter() { + for (module_name, exposed, region) in imported.into_iter() { let cloned_module_name = module_name.clone(); let module_id = module_ids.get_or_insert(&module_name.into()); @@ -618,7 +651,7 @@ fn send_interface_header<'a>( // // We must *not* add them to scope yet, or else the Defs will // incorrectly think they're shadowing them! - for loc_exposed in header.exposes.iter() { + for loc_exposed in exposes.iter() { // Use get_or_insert here because the ident_ids may already // created an IdentId for this, when it was imported exposed // in a dependent module. @@ -629,7 +662,7 @@ fn send_interface_header<'a>( let ident_id = ident_ids.get_or_insert(&loc_exposed.value.as_str().into()); let symbol = Symbol::new(home, ident_id); - exposes.push(symbol); + exposed.push(symbol); } if cfg!(debug_assertions) { @@ -648,7 +681,7 @@ fn send_interface_header<'a>( // and also add any exposed values to scope. // // e.g. for `imports [ Foo.{ bar } ]`, add `Foo` to deps_by_name and `bar` to scope. - for (module_name, exposed, region) in imports.into_iter() { + for (module_name, exposed, region) in imported.into_iter() { let module_id = module_ids.get_or_insert(&module_name.clone().into()); deps_by_name.insert(module_name, module_id); @@ -672,11 +705,11 @@ fn send_interface_header<'a>( // // We must *not* add them to scope yet, or else the Defs will // incorrectly think they're shadowing them! - for loc_exposed in header.exposes.iter() { + for loc_exposed in exposes.iter() { let ident_id = ident_ids.add(loc_exposed.value.as_str().into()); let symbol = Symbol::new(home, ident_id); - exposes.push(symbol); + exposed.push(symbol); } if cfg!(debug_assertions) { @@ -712,7 +745,7 @@ fn send_interface_header<'a>( module_name: declared_name, imported_modules, deps_by_name, - exposes, + exposes: exposed, src, exposed_imports: scope, })) @@ -860,6 +893,7 @@ fn solve_module( aliases: module.aliases, }; + let src = module.src; let mut subs = Subs::new(var_store.into()); for (var, name) in module.rigid_variables { @@ -898,10 +932,10 @@ fn solve_module( // annotations which are decoupled from our Subs, because that's how // other modules will generate constraints for imported values // within the context of their own Subs. - for (symbol, var) in exposed_vars_by_symbol { - let solved_type = SolvedType::new(&solved_subs, var); + for (symbol, var) in exposed_vars_by_symbol.iter() { + let solved_type = SolvedType::new(&solved_subs, *var); - solved_types.insert(symbol, solved_type); + solved_types.insert(*symbol, solved_type); } tokio::spawn(async move { @@ -909,8 +943,10 @@ fn solve_module( // Send the subs to the main thread for processing, tx.send(Msg::Solved { + src, module_id: home, subs: Arc::new(solved_subs), + exposed_vars_by_symbol, solved_types, problems, aliases: env.aliases, @@ -1044,6 +1080,7 @@ fn parse_and_constrain( aliases, rigid_variables, imported_modules: header.imported_modules, + src: header.src, }; (module, ident_ids, constraint, problems) diff --git a/compiler/load/tests/fixtures/build/app_with_deps/AStar.roc b/compiler/load/tests/fixtures/build/app_with_deps/AStar.roc new file mode 100644 index 0000000000..0901f80fe9 --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/AStar.roc @@ -0,0 +1,111 @@ +interface AStar + exposes [ initialModel, reconstructPath, updateCost, cheapestOpen, astar, findPath ] + imports [] + + +# a port of https://github.com/krisajenkins/elm-astar/blob/2.1.3/src/AStar/Generalised.elm + +Model position : + { evaluated : Set position + , openSet : Set position + , costs : Map.Map position Float + , cameFrom : Map.Map position position + } + + +initialModel : position -> Model position +initialModel = \start -> + { evaluated : Set.empty + , openSet : Set.singleton start + , costs : Map.singleton start 0.0 + , cameFrom : Map.empty + } + + +cheapestOpen : (position -> Float), Model position -> Result position [ KeyNotFound ]* +cheapestOpen = \costFunction, model -> + + folder = \position, resSmallestSoFar -> + when Map.get model.costs position is + Err e -> + Err e + + Ok cost -> + positionCost = costFunction position + + when resSmallestSoFar is + Err _ -> Ok { position, cost: cost + positionCost } + Ok smallestSoFar -> + if positionCost + cost < smallestSoFar.cost then + Ok { position, cost: cost + positionCost } + + else + Ok smallestSoFar + + Set.foldl model.openSet folder (Err KeyNotFound) + |> Result.map (\x -> x.position) + + + +reconstructPath : Map position position, position -> List position +reconstructPath = \cameFrom, goal -> + when Map.get cameFrom goal is + Err KeyNotFound -> + [] + + Ok next -> + List.push (reconstructPath cameFrom next) goal + +updateCost : position, position, Model position -> Model position +updateCost = \current, neighbour, model -> + newCameFrom = Map.insert model.cameFrom neighbour current + + newCosts = Map.insert model.costs neighbour distanceTo + + distanceTo = reconstructPath newCameFrom neighbour + |> List.len + |> Num.toFloat + + newModel = { model & costs : newCosts , cameFrom : newCameFrom } + + when Map.get model.costs neighbour is + Err KeyNotFound -> + newModel + + Ok previousDistance -> + if distanceTo < previousDistance then + newModel + + else + model + + +findPath : { costFunction: (position, position -> Float), moveFunction: (position -> Set position), start : position, end : position } -> Result (List position) [ KeyNotFound ]* +findPath = \{ costFunction, moveFunction, start, end } -> + astar costFunction moveFunction end (initialModel start) + + +astar : (position, position -> Float), (position -> Set position), position, Model position -> [ Err [ KeyNotFound ]*, Ok (List position) ]* +astar = \costFn, moveFn, goal, model -> + when cheapestOpen (\position -> costFn goal position) model is + Err _ -> + Err KeyNotFound + + Ok current -> + if current == goal then + Ok (reconstructPath model.cameFrom goal) + + else + + modelPopped = { model & openSet : Set.remove model.openSet current, evaluated : Set.insert model.evaluated current } + + neighbours = moveFn current + + newNeighbours = Set.diff neighbours modelPopped.evaluated + + modelWithNeighbours = { modelPopped & openSet : Set.union modelPopped.openSet newNeighbours } + + modelWithCosts = Set.foldl newNeighbours (\nb, md -> updateCost current nb md) modelWithNeighbours + + astar costFn moveFn goal modelWithCosts + diff --git a/compiler/load/tests/fixtures/build/app_with_deps/Dep1.roc b/compiler/load/tests/fixtures/build/app_with_deps/Dep1.roc new file mode 100644 index 0000000000..105423b9fd --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/Dep1.roc @@ -0,0 +1,15 @@ +interface Dep1 + exposes [ three, str, Unit, Identity, one, two ] + imports [ Dep3.Blah.{ foo } ] + +one = 1 + +two = foo + +three = 3.0 + +str = "string!" + +Unit : [ Unit ] + +Identity a : [ Identity a ] diff --git a/compiler/load/tests/fixtures/build/app_with_deps/Dep2.roc b/compiler/load/tests/fixtures/build/app_with_deps/Dep2.roc new file mode 100644 index 0000000000..af810ae052 --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/Dep2.roc @@ -0,0 +1,10 @@ +interface Dep2 + exposes [ one, two, blah ] + imports [ Dep3.Blah.{ foo, bar } ] + +one = 1 + +blah = foo + +two = 2.0 + diff --git a/compiler/load/tests/fixtures/build/app_with_deps/Dep3/Blah.roc b/compiler/load/tests/fixtures/build/app_with_deps/Dep3/Blah.roc new file mode 100644 index 0000000000..225ba10c60 --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/Dep3/Blah.roc @@ -0,0 +1,10 @@ +interface Dep3.Blah + exposes [ one, two, foo, bar ] + imports [] + +one = 1 + +two = 2 + +foo = "foo from Dep3" +bar = "bar from Dep3" diff --git a/compiler/load/tests/fixtures/build/app_with_deps/ImportAlias.roc b/compiler/load/tests/fixtures/build/app_with_deps/ImportAlias.roc new file mode 100644 index 0000000000..945a2168bb --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/ImportAlias.roc @@ -0,0 +1,7 @@ +interface ImportAlias + exposes [ unit ] + imports [ Dep1 ] + +unit : Dep1.Unit +unit = Unit + diff --git a/compiler/load/tests/fixtures/build/app_with_deps/ManualAttr.roc b/compiler/load/tests/fixtures/build/app_with_deps/ManualAttr.roc new file mode 100644 index 0000000000..7fd1e010c8 --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/ManualAttr.roc @@ -0,0 +1,18 @@ +interface ManualAttr + exposes [] + imports [] + +# manually replicates the Attr wrapping that uniqueness inference uses, to try and find out why they are different +# It is very important that there are no signatures here! elm uses an optimization that leads to less copying when +# signatures are given. + +map = + unAttr = \Attr _ foobar -> foobar + + r = Attr unknown "bar" + + s = Attr unknown2 { left : Attr Shared "foo" } + + when True is + _ -> { y : r } + _ -> { y : (unAttr s).left } diff --git a/compiler/load/tests/fixtures/build/app_with_deps/OneDep.roc b/compiler/load/tests/fixtures/build/app_with_deps/OneDep.roc new file mode 100644 index 0000000000..f125b666bb --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/OneDep.roc @@ -0,0 +1,5 @@ +interface OneDep + exposes [ str ] + imports [ Dep3.Blah.{ foo } ] + +str = foo diff --git a/compiler/load/tests/fixtures/build/app_with_deps/Primary.roc b/compiler/load/tests/fixtures/build/app_with_deps/Primary.roc new file mode 100644 index 0000000000..8c9552d1ba --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/Primary.roc @@ -0,0 +1,31 @@ +app Primary + provides [ blah2, blah3, str, alwaysThree, identity, z, w, succeed, withDefault, yay ] + imports [ Dep1, Dep2.{ two, foo }, Dep3.Blah.{ bar }, Res ] + +blah2 = Dep2.two +blah3 = bar + +str = Dep1.str + +# alwaysThree = \_ -> Dep1.three # TODO FIXME for some reason this infers as a circular type +alwaysThree = \_ -> "foo" + +identity = \a -> a + +z = identity (alwaysThree {}) + +w : Dep1.Identity {} +w = Identity {} + +succeed : a -> Dep1.Identity a +succeed = \x -> Identity x + +withDefault = Res.withDefault + +yay : Res.Res {} err +yay = + ok = Ok "foo" + + f = \_ -> {} + + Res.map ok f diff --git a/examples/quicksort/qs.roc b/compiler/load/tests/fixtures/build/app_with_deps/Quicksort.roc similarity index 95% rename from examples/quicksort/qs.roc rename to compiler/load/tests/fixtures/build/app_with_deps/Quicksort.roc index 96798ecdbd..1d363a1b5b 100644 --- a/examples/quicksort/qs.roc +++ b/compiler/load/tests/fixtures/build/app_with_deps/Quicksort.roc @@ -1,3 +1,7 @@ +app Quicksort + provides [ swap, partition, quicksort ] + imports [] + quicksort : List (Num a), Int, Int -> List (Num a) quicksort = \list, low, high -> when partition low high list is @@ -43,5 +47,3 @@ partition = \low, high, initialList -> Err _ -> Pair (low - 1) initialList - -quicksort [ 7, 4, 9 ] diff --git a/compiler/load/tests/fixtures/build/app_with_deps/Records.roc b/compiler/load/tests/fixtures/build/app_with_deps/Records.roc new file mode 100644 index 0000000000..c4f069fec3 --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/Records.roc @@ -0,0 +1,8 @@ +interface Records + exposes [ intVal ] + imports [] + +intVal = + foo = \{ x } -> x + + foo { x: 5 } diff --git a/compiler/load/tests/fixtures/build/app_with_deps/Res.roc b/compiler/load/tests/fixtures/build/app_with_deps/Res.roc new file mode 100644 index 0000000000..190dd98603 --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/Res.roc @@ -0,0 +1,32 @@ +interface Res + exposes [ Res, withDefault, map, andThen, ConsList ] + imports [] + +Res ok err : [ Ok ok, Err err ] + +ConsList a : [ Cons a (ConsList a), Nil ] + +# TODO FIXME for some reason, exposing this causes a stack overflow +# listMap : ConsList a, (a -> b) -> ConsList b +# listMap = \list, f -> + # when list is + # Nil -> Nil + # Cons x xs -> Cons (f x) (listMap xs f) + +map : Res a err, (a -> b) -> Res b err +map = \result, transform -> + when result is + Ok ok -> Ok (transform ok) + Err err -> Err err + +withDefault : Res a err, a -> a +withDefault = \result, default -> + when result is + Ok ok -> ok + Err _ -> default + +andThen : Res a err, (a -> Res b err) -> Res b err +andThen = \result, transform -> + when result is + Ok ok -> transform ok + Err err -> Err err diff --git a/compiler/load/tests/fixtures/build/app_with_deps/WithBuiltins.roc b/compiler/load/tests/fixtures/build/app_with_deps/WithBuiltins.roc new file mode 100644 index 0000000000..37300efb8c --- /dev/null +++ b/compiler/load/tests/fixtures/build/app_with_deps/WithBuiltins.roc @@ -0,0 +1,19 @@ +interface WithBuiltins + exposes [ floatTest, divisionFn, divisionTest, intTest, constantNum, fromDep2, divDep1ByDep2 ] + imports [ Dep1, Dep2.{ two } ] + +floatTest = Float.highest + +divisionFn = Float.div + +x = 5.0 + +divisionTest = Float.highest / x + +intTest = Int.highest + +constantNum = 5 + +fromDep2 = Dep2.two + +divDep1ByDep2 = Dep1.three / fromDep2 diff --git a/compiler/load/tests/fixtures/build/interface_with_deps/Primary.roc b/compiler/load/tests/fixtures/build/interface_with_deps/Primary.roc index 4015d971c2..17289a8e7b 100644 --- a/compiler/load/tests/fixtures/build/interface_with_deps/Primary.roc +++ b/compiler/load/tests/fixtures/build/interface_with_deps/Primary.roc @@ -3,7 +3,7 @@ interface Primary imports [ Dep1, Dep2.{ two, foo }, Dep3.Blah.{ bar }, Res ] blah2 = Dep2.two -blah3 = bar # TODO FIXME does work as Dep3.Blah.bar, some scoping issue +blah3 = bar str = Dep1.str @@ -12,11 +12,7 @@ alwaysThree = \_ -> "foo" identity = \a -> a -# z = identity (alwaysThree {}) # TODO FIXME for some reason this infers as a circular type -# z = identity 3 # TODO FIXME for some reason this also infers as a circular type - -z : Dep1.Unit -z = Unit +z = identity (alwaysThree {}) w : Dep1.Identity {} w = Identity {} diff --git a/compiler/load/tests/test_load.rs b/compiler/load/tests/test_load.rs index 58923ad6ce..058673bd7a 100644 --- a/compiler/load/tests/test_load.rs +++ b/compiler/load/tests/test_load.rs @@ -212,7 +212,7 @@ mod test_load { } #[test] - fn load_and_typecheck_quicksort() { + fn iface_quicksort() { test_async(async { let subs_by_module = MutMap::default(); let loaded_module = @@ -229,6 +229,23 @@ mod test_load { }); } + #[test] + fn app_quicksort() { + test_async(async { + let subs_by_module = MutMap::default(); + let loaded_module = load_fixture("app_with_deps", "Quicksort", subs_by_module).await; + + expect_types( + loaded_module, + hashmap! { + "swap" => "Int, Int, List a -> List a", + "partition" => "Int, Int, List (Num a) -> [ Pair Int (List (Num a)) ]", + "quicksort" => "List (Num a), Int, Int -> List (Num a)", + }, + ); + }); + } + #[test] fn load_astar() { test_async(async { @@ -266,7 +283,7 @@ mod test_load { } #[test] - fn load_dep_types() { + fn iface_dep_types() { test_async(async { let subs_by_module = MutMap::default(); let loaded_module = @@ -280,7 +297,31 @@ mod test_load { "str" => "Str", "alwaysThree" => "* -> Str", "identity" => "a -> a", - "z" => "Dep1.Unit", + "z" => "Str", + "w" => "Dep1.Identity {}", + "succeed" => "a -> Dep1.Identity a", + "yay" => "Res.Res {} err", + "withDefault" => "Res.Res a *, a -> a", + }, + ); + }); + } + + #[test] + fn app_dep_types() { + test_async(async { + let subs_by_module = MutMap::default(); + let loaded_module = load_fixture("app_with_deps", "Primary", subs_by_module).await; + + expect_types( + loaded_module, + hashmap! { + "blah2" => "Float", + "blah3" => "Str", + "str" => "Str", + "alwaysThree" => "* -> Str", + "identity" => "a -> a", + "z" => "Str", "w" => "Dep1.Identity {}", "succeed" => "a -> Dep1.Identity a", "yay" => "Res.Res {} err", diff --git a/compiler/load/tests/test_uniq_load.rs b/compiler/load/tests/test_uniq_load.rs index e0e39db172..785b98ebe1 100644 --- a/compiler/load/tests/test_uniq_load.rs +++ b/compiler/load/tests/test_uniq_load.rs @@ -275,7 +275,7 @@ mod test_uniq_load { "str" => "Attr * Str", "alwaysThree" => "Attr * (* -> Attr * Str)", "identity" => "Attr * (a -> a)", - "z" => "Attr * Dep1.Unit", + "z" => "Attr * Str", "w" => "Attr * (Dep1.Identity (Attr * {}))", "succeed" => "Attr * (Attr b a -> Attr * (Dep1.Identity (Attr b a)))", "yay" => "Attr * (Res.Res (Attr * {}) (Attr * err))", diff --git a/compiler/module/Cargo.toml b/compiler/module/Cargo.toml index 0afa6be379..b6893e575a 100644 --- a/compiler/module/Cargo.toml +++ b/compiler/module/Cargo.toml @@ -12,6 +12,6 @@ inlinable_string = "0.1.0" lazy_static = "1.4" [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" diff --git a/compiler/mono/Cargo.toml b/compiler/mono/Cargo.toml index f2e3bb270b..aa86ddedff 100644 --- a/compiler/mono/Cargo.toml +++ b/compiler/mono/Cargo.toml @@ -19,7 +19,7 @@ roc_constrain = { path = "../constrain" } roc_builtins = { path = "../builtins" } roc_parse = { path = "../parse" } roc_solve = { path = "../solve" } -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/mono/src/expr.rs b/compiler/mono/src/expr.rs index 45b33c5193..f59d67d01b 100644 --- a/compiler/mono/src/expr.rs +++ b/compiler/mono/src/expr.rs @@ -10,10 +10,27 @@ use roc_region::all::{Located, Region}; use roc_types::subs::{Content, ContentHash, FlatType, Subs, Variable}; use std::hash::Hash; +#[derive(Clone, Debug, PartialEq)] +pub struct PartialProc<'a> { + pub annotation: Variable, + pub patterns: Vec<'a, Symbol>, + pub body: roc_can::expr::Expr, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Proc<'a> { + pub name: Symbol, + pub args: &'a [(Layout<'a>, Symbol)], + pub body: Expr<'a>, + pub closes_over: Layout<'a>, + pub ret_layout: Layout<'a>, +} + #[derive(Clone, Debug, PartialEq, Default)] pub struct Procs<'a> { user_defined: MutMap>, anonymous: MutMap>>, + specializations: MutMap>)>, builtin: MutSet, } @@ -28,14 +45,11 @@ impl<'a> Procs<'a> { fn insert_specialization( &mut self, - symbol: Symbol, hash: ContentHash, spec_name: Symbol, proc: Option>, ) { - self.user_defined - .get_mut(&symbol) - .map(|partial_proc| partial_proc.specializations.insert(hash, (spec_name, proc))); + self.specializations.insert(hash, (spec_name, proc)); } fn get_user_defined(&self, symbol: Symbol) -> Option<&PartialProc<'a>> { @@ -44,11 +58,7 @@ impl<'a> Procs<'a> { pub fn len(&self) -> usize { let anonymous: usize = self.anonymous.len(); - let user_defined: usize = self - .user_defined - .values() - .map(|v| v.specializations.len()) - .sum(); + let user_defined: usize = self.specializations.len(); anonymous + user_defined } @@ -64,10 +74,8 @@ impl<'a> Procs<'a> { pub fn as_map(&self) -> MutMap>> { let mut result = MutMap::default(); - for partial_proc in self.user_defined.values() { - for (_, (symbol, opt_proc)) in partial_proc.specializations.clone().into_iter() { - result.insert(symbol, opt_proc); - } + for (symbol, opt_proc) in self.specializations.values() { + result.insert(*symbol, opt_proc.clone()); } for (symbol, proc) in self.anonymous.clone().into_iter() { @@ -82,23 +90,6 @@ impl<'a> Procs<'a> { } } -#[derive(Clone, Debug, PartialEq)] -pub struct PartialProc<'a> { - pub annotation: Variable, - pub patterns: Vec<'a, Symbol>, - pub body: roc_can::expr::Expr, - pub specializations: MutMap>)>, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Proc<'a> { - pub name: Symbol, - pub args: &'a [(Layout<'a>, Symbol)], - pub body: Expr<'a>, - pub closes_over: Layout<'a>, - pub ret_layout: Layout<'a>, -} - pub struct Env<'a, 'i> { pub arena: &'a Bump, pub subs: &'a mut Subs, @@ -458,7 +449,6 @@ fn from_can<'a>( annotation, patterns: arg_symbols, body: body.value, - specializations: MutMap::default(), }, ); symbol @@ -831,8 +821,9 @@ fn from_can<'a>( elems: elems.into_bump_slice(), } } + Accessor { .. } => todo!("record accessor"), + Update { .. } => todo!("record update"), RuntimeError(error) => Expr::RuntimeError(env.arena.alloc(format!("{:?}", error))), - other => panic!("TODO convert canonicalized {:?} to mono::Expr", other), } } @@ -1304,36 +1295,42 @@ fn call_by_name<'a>( Vec<'a, Symbol>, )>; - let specialized_proc_name = if let Some(partial_proc) = procs.get_user_defined(proc_name) { - let content_hash = ContentHash::from_var(fn_var, env.subs); + let specialized_proc_name = match procs.get_user_defined(proc_name) { + Some(partial_proc) => { + let content_hash = ContentHash::from_var(fn_var, env.subs); - if let Some(specialization) = partial_proc.specializations.get(&content_hash) { + match procs.specializations.get(&content_hash) { + Some(specialization) => { + opt_specialize_body = None; + + // a specialization with this type hash already exists, use its symbol + specialization.0 + } + None => { + opt_specialize_body = Some(( + content_hash, + partial_proc.annotation, + partial_proc.body.clone(), + partial_proc.patterns.clone(), + )); + + // generate a symbol for this specialization + env.fresh_symbol() + } + } + } + None => { opt_specialize_body = None; - // a specialization with this type hash already exists, use its symbol - specialization.0 - } else { - opt_specialize_body = Some(( - content_hash, - partial_proc.annotation, - partial_proc.body.clone(), - partial_proc.patterns.clone(), - )); - - // generate a symbol for this specialization - env.fresh_symbol() + // This happens for built-in symbols (they are never defined as a Closure) + procs.insert_builtin(proc_name); + proc_name } - } else { - opt_specialize_body = None; - - // This happens for built-in symbols (they are never defined as a Closure) - procs.insert_builtin(proc_name); - proc_name }; if let Some((content_hash, annotation, body, loc_patterns)) = opt_specialize_body { // register proc, so specialization doesn't loop infinitely - procs.insert_specialization(proc_name, content_hash, specialized_proc_name, None); + procs.insert_specialization(content_hash, specialized_proc_name, None); let arg_vars = loc_args.iter().map(|v| v.0).collect::>(); @@ -1350,7 +1347,7 @@ fn call_by_name<'a>( ) .ok(); - procs.insert_specialization(proc_name, content_hash, specialized_proc_name, proc); + procs.insert_specialization(content_hash, specialized_proc_name, proc); } // generate actual call diff --git a/compiler/parse/Cargo.toml b/compiler/parse/Cargo.toml index 776f5b10cb..4a41599a8e 100644 --- a/compiler/parse/Cargo.toml +++ b/compiler/parse/Cargo.toml @@ -12,7 +12,7 @@ bumpalo = { version = "3.2", features = ["collections"] } inlinable_string = "0.1.0" [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" indoc = "0.3.3" quickcheck = "0.8" quickcheck_macros = "0.8" diff --git a/compiler/parse/src/ast.rs b/compiler/parse/src/ast.rs index 91583fa2fd..d24ea05ad7 100644 --- a/compiler/parse/src/ast.rs +++ b/compiler/parse/src/ast.rs @@ -36,9 +36,14 @@ pub struct WhenBranch<'a> { #[derive(Clone, Debug, PartialEq)] pub struct AppHeader<'a> { + pub name: Loc>, + pub provides: Vec<'a, Loc>>, pub imports: Vec<'a, Loc>>, // Potential comments and newlines - these will typically all be empty. + pub after_interface: &'a [CommentOrNewline<'a>], + pub before_provides: &'a [CommentOrNewline<'a>], + pub after_provides: &'a [CommentOrNewline<'a>], pub before_imports: &'a [CommentOrNewline<'a>], pub after_imports: &'a [CommentOrNewline<'a>], } diff --git a/compiler/parse/src/expr.rs b/compiler/parse/src/expr.rs index c5e841db4b..b09240a658 100644 --- a/compiler/parse/src/expr.rs +++ b/compiler/parse/src/expr.rs @@ -1244,10 +1244,14 @@ pub fn ident_etc<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> { move |arena, state, (loc_ident, opt_extras)| { // This appears to be a var, keyword, or function application. match opt_extras { - (Some(_loc_args), Some((_spaces_before_equals, Either::First(_equals_indent)))) => { - // We got args with an '=' after them, e.g. `foo a b = ...` - // This is a syntax error! - panic!("TODO gracefully handle parse error for defs like `foo a b = ...`"); + (Some(loc_args), Some((_spaces_before_equals, Either::First(_equals_indent)))) => { + // We got args with an '=' after them, e.g. `foo a b = ...` This is a syntax error! + let region = Region::across_all(loc_args.iter().map(|v| &v.region)); + let fail = Fail { + attempting: state.attempting, + reason: FailReason::ArgumentsBeforeEquals(region), + }; + Err((fail, state)) } (None, Some((spaces_before_equals, Either::First(equals_indent)))) => { // We got '=' with no args before it diff --git a/compiler/parse/src/module.rs b/compiler/parse/src/module.rs index 3a4067749b..6f9e151b71 100644 --- a/compiler/parse/src/module.rs +++ b/compiler/parse/src/module.rs @@ -127,9 +127,30 @@ pub fn module_name<'a>() -> impl Parser<'a, ModuleName<'a>> { #[inline(always)] fn app_header<'a>() -> impl Parser<'a, AppHeader<'a>> { - move |_, _| { - panic!("TODO parse app header"); - } + parser::map( + and!( + skip_first!(string("app"), and!(space1(1), loc!(module_name()))), + and!(provides(), imports()) + ), + |( + (after_interface, name), + ( + ((before_provides, after_provides), provides), + ((before_imports, after_imports), imports), + ), + )| { + AppHeader { + name, + provides, + imports, + after_interface, + before_provides, + after_provides, + before_imports, + after_imports, + } + }, + ) } #[inline(always)] @@ -137,6 +158,20 @@ pub fn module_defs<'a>() -> impl Parser<'a, Vec<'a, Located>>> { zero_or_more!(space0_around(loc(def(0)), 0)) } +#[inline(always)] +fn provides<'a>() -> impl Parser< + 'a, + ( + (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), + Vec<'a, Located>>, + ), +> { + and!( + and!(skip_second!(space1(1), string("provides")), space1(1)), + collection!(char('['), loc!(exposes_entry()), char(','), char(']'), 1) + ) +} + #[inline(always)] fn exposes<'a>() -> impl Parser< 'a, diff --git a/compiler/parse/src/parser.rs b/compiler/parse/src/parser.rs index 9e33207ec0..5f4364728d 100644 --- a/compiler/parse/src/parser.rs +++ b/compiler/parse/src/parser.rs @@ -190,6 +190,7 @@ pub enum FailReason { Eof(Region), InvalidPattern, ReservedKeyword(Region), + ArgumentsBeforeEquals(Region), } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/compiler/problem/Cargo.toml b/compiler/problem/Cargo.toml index 7568191c1b..0054808f0f 100644 --- a/compiler/problem/Cargo.toml +++ b/compiler/problem/Cargo.toml @@ -12,7 +12,7 @@ roc_parse = { path = "../parse" } inlinable_string = "0.1.0" [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/problem/src/can.rs b/compiler/problem/src/can.rs index a4c50ddd1a..0035b7a5b8 100644 --- a/compiler/problem/src/can.rs +++ b/compiler/problem/src/can.rs @@ -1,6 +1,6 @@ use inlinable_string::InlinableString; use roc_collections::all::MutSet; -use roc_module::ident::Ident; +use roc_module::ident::{Ident, Lowercase, TagName}; use roc_module::symbol::{ModuleId, Symbol}; use roc_parse::operator::BinOp; use roc_parse::pattern::PatternType; @@ -21,6 +21,30 @@ pub enum Problem { original_region: Region, shadow: Located, }, + CyclicAlias(Symbol, Region, Vec), + PhantomTypeArgument { + alias: Symbol, + variable_region: Region, + variable_name: Lowercase, + }, + DuplicateRecordFieldValue { + field_name: Lowercase, + record_region: Region, + field_region: Region, + replaced_region: Region, + }, + DuplicateRecordFieldType { + field_name: Lowercase, + record_region: Region, + field_region: Region, + replaced_region: Region, + }, + DuplicateTag { + tag_name: TagName, + tag_union_region: Region, + tag_region: Region, + replaced_region: Region, + }, RuntimeError(RuntimeError), } diff --git a/compiler/region/src/all.rs b/compiler/region/src/all.rs index 0ab9ad2370..372f503ea0 100644 --- a/compiler/region/src/all.rs +++ b/compiler/region/src/all.rs @@ -47,6 +47,10 @@ impl Region { } } + pub fn is_empty(&self) -> bool { + self.end_line == self.start_line && self.start_col == self.end_col + } + pub fn span_across(start: &Region, end: &Region) -> Self { Region { start_line: start.start_line, diff --git a/compiler/reporting/Cargo.toml b/compiler/reporting/Cargo.toml index 1392573d22..039a98ef31 100644 --- a/compiler/reporting/Cargo.toml +++ b/compiler/reporting/Cargo.toml @@ -27,7 +27,7 @@ roc_constrain = { path = "../constrain" } roc_builtins = { path = "../builtins" } roc_problem = { path = "../problem" } roc_parse = { path = "../parse" } -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/reporting/src/error/canonicalize.rs b/compiler/reporting/src/error/canonicalize.rs new file mode 100644 index 0000000000..9d691a7ffb --- /dev/null +++ b/compiler/reporting/src/error/canonicalize.rs @@ -0,0 +1,397 @@ +use roc_collections::all::MutSet; +use roc_problem::can::PrecedenceProblem::BothNonAssociative; +use roc_problem::can::{Problem, RuntimeError}; +use std::path::PathBuf; + +use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder}; +use ven_pretty::DocAllocator; + +pub fn can_problem<'b>( + alloc: &'b RocDocAllocator<'b>, + filename: PathBuf, + problem: Problem, +) -> Report<'b> { + let doc = match problem { + Problem::UnusedDef(symbol, region) => { + let line = + r#" then remove it so future readers of your code don't wonder why it is there."#; + + alloc.stack(vec![ + alloc + .symbol_unqualified(symbol) + .append(alloc.reflow(" is not used anywhere in your code.")), + alloc.region(region), + alloc + .reflow("If you didn't intend on using ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(line)), + ]) + } + Problem::UnusedImport(module_id, region) => alloc.concat(vec![ + alloc.reflow("Nothing from "), + alloc.module(module_id), + alloc.reflow(" is used in this module."), + alloc.region(region), + alloc.reflow("Since "), + alloc.module(module_id), + alloc.reflow(" isn't used, you don't need to import it."), + ]), + Problem::UnusedArgument(closure_symbol, argument_symbol, region) => { + let line = "\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used."; + + alloc.concat(vec![ + alloc.symbol_unqualified(closure_symbol), + alloc.reflow(" doesn't use "), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow("."), + alloc.region(region), + alloc.reflow("If you don't need "), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(", then you can just remove it. However, if you really do need "), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(" as an argument of "), + alloc.symbol_unqualified(closure_symbol), + alloc.reflow(", prefix it with an underscore, like this: \"_"), + alloc.symbol_unqualified(argument_symbol), + alloc.reflow(line), + ]) + } + Problem::PrecedenceProblem(BothNonAssociative(region, left_bin_op, right_bin_op)) => alloc + .stack(vec![ + if left_bin_op.value == right_bin_op.value { + alloc.concat(vec![ + alloc.reflow("Using more than one "), + alloc.binop(left_bin_op.value), + alloc.reflow(concat!( + " like this requires parentheses,", + " to clarify how things should be grouped.", + )), + ]) + } else { + alloc.concat(vec![ + alloc.reflow("Using "), + alloc.binop(left_bin_op.value), + alloc.reflow(" and "), + alloc.binop(right_bin_op.value), + alloc.reflow(concat!( + " together requires parentheses, ", + "to clarify how they should be grouped." + )), + ]) + }, + alloc.region(region), + ]), + Problem::UnsupportedPattern(pattern_type, region) => { + use roc_parse::pattern::PatternType::*; + + let this_thing = match pattern_type { + TopLevelDef => "a top-level definition:", + DefExpr => "a value definition:", + FunctionArg => "function arguments:", + WhenBranch => unreachable!("all patterns are allowed in a When"), + }; + + let suggestion = vec![ + alloc.reflow( + "Patterns like this don't cover all possible shapes of the input type. Use a ", + ), + alloc.keyword("when"), + alloc.reflow(" ... "), + alloc.keyword("is"), + alloc.reflow(" instead."), + ]; + + alloc.stack(vec![ + alloc + .reflow("This pattern is not allowed in ") + .append(alloc.reflow(this_thing)), + alloc.region(region), + alloc.concat(suggestion), + ]) + } + Problem::ShadowingInAnnotation { + original_region, + shadow, + } => pretty_runtime_error( + alloc, + RuntimeError::Shadowing { + original_region, + shadow, + }, + ), + Problem::CyclicAlias(symbol, region, others) => { + let (doc, title) = crate::error::r#type::cyclic_alias(alloc, symbol, region, others); + + return Report { + filename, + title, + doc, + }; + } + Problem::PhantomTypeArgument { + alias, + variable_region, + variable_name, + } => alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("The "), + alloc.type_variable(variable_name), + alloc.reflow(" type variable is not used in the "), + alloc.symbol_unqualified(alias), + alloc.reflow(" alias definition:"), + ]), + alloc.region(variable_region), + alloc.reflow("Roc does not allow unused type parameters!"), + // TODO add link to this guide section + alloc.hint().append(alloc.reflow( + "If you want an unused type parameter (a so-called \"phantom type\"), \ + read the guide section on phantom data.", + )), + ]), + Problem::DuplicateRecordFieldValue { + field_name, + field_region, + record_region, + replaced_region, + } => alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("This record defines the "), + alloc.record_field(field_name.clone()), + alloc.reflow(" field twice!"), + ]), + alloc.region_all_the_things( + record_region, + replaced_region, + field_region, + Annotation::Error, + ), + alloc.reflow("In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + record_region, + field_region, + field_region, + Annotation::TypoSuggestion, + ), + alloc.concat(vec![ + alloc.reflow("For clarity, remove the previous "), + alloc.record_field(field_name), + alloc.reflow(" definitions from this record."), + ]), + ]), + Problem::DuplicateRecordFieldType { + field_name, + field_region, + record_region, + replaced_region, + } => alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("This record type defines the "), + alloc.record_field(field_name.clone()), + alloc.reflow(" field twice!"), + ]), + alloc.region_all_the_things( + record_region, + replaced_region, + field_region, + Annotation::Error, + ), + alloc.reflow("In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + record_region, + field_region, + field_region, + Annotation::TypoSuggestion, + ), + alloc.concat(vec![ + alloc.reflow("For clarity, remove the previous "), + alloc.record_field(field_name), + alloc.reflow(" definitions from this record type."), + ]), + ]), + Problem::DuplicateTag { + tag_name, + tag_union_region, + tag_region, + replaced_region, + } => alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("This tag union type defines the "), + alloc.tag_name(tag_name.clone()), + alloc.reflow(" tag twice!"), + ]), + alloc.region_all_the_things( + tag_union_region, + replaced_region, + tag_region, + Annotation::Error, + ), + alloc.reflow("In the rest of the program, I will only use the latter definition:"), + alloc.region_all_the_things( + tag_union_region, + tag_region, + tag_region, + Annotation::TypoSuggestion, + ), + alloc.concat(vec![ + alloc.reflow("For clarity, remove the previous "), + alloc.tag_name(tag_name), + alloc.reflow(" definitions from this tag union type."), + ]), + ]), + Problem::RuntimeError(runtime_error) => pretty_runtime_error(alloc, runtime_error), + }; + + Report { + title: "SYNTAX PROBLEM".to_string(), + filename, + doc, + } +} + +fn pretty_runtime_error<'b>( + alloc: &'b RocDocAllocator<'b>, + runtime_error: RuntimeError, +) -> RocDocBuilder<'b> { + match runtime_error { + RuntimeError::Shadowing { + original_region, + shadow, + } => { + let line = r#"Since these variables have the same name, it's easy to use the wrong one on accident. Give one of them a new name."#; + + alloc.stack(vec![ + alloc + .text("The ") + .append(alloc.ident(shadow.value)) + .append(alloc.reflow(" name is first defined here:")), + alloc.region(original_region), + alloc.reflow("But then it's defined a second time here:"), + alloc.region(shadow.region), + alloc.reflow(line), + ]) + } + + RuntimeError::LookupNotInScope(loc_name, options) => { + not_found(alloc, loc_name.region, &loc_name.value, "value", options) + } + RuntimeError::CircularDef(mut idents, regions) => { + let first = idents.remove(0); + + if idents.is_empty() { + alloc + .reflow("The ") + .append(alloc.ident(first.value.clone())) + .append(alloc.reflow( + " value is defined directly in terms of itself, causing an infinite loop.", + )) + // TODO "are you trying to mutate a variable? + // TODO hint? + } else { + alloc.stack(vec![ + alloc + .reflow("The ") + .append(alloc.ident(first.value.clone())) + .append( + alloc.reflow(" definition is causing a very tricky infinite loop:"), + ), + alloc.region(regions[0].0), + alloc + .reflow("The ") + .append(alloc.ident(first.value.clone())) + .append(alloc.reflow( + " value depends on itself through the following chain of definitions:", + )), + crate::report::cycle( + alloc, + 4, + alloc.ident(first.value), + idents + .into_iter() + .map(|ident| alloc.ident(ident.value)) + .collect::>(), + ), + // TODO hint? + ]) + } + } + other => { + // // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! + // UnsupportedPattern(Region), + // UnrecognizedFunctionName(Located), + // SymbolNotExposed { + // module_name: InlinableString, + // ident: InlinableString, + // region: Region, + // }, + // ModuleNotImported { + // module_name: InlinableString, + // ident: InlinableString, + // region: Region, + // }, + // InvalidPrecedence(PrecedenceProblem, Region), + // MalformedIdentifier(Box, Region), + // MalformedClosure(Region), + // FloatOutsideRange(Box), + // IntOutsideRange(Box), + // InvalidHex(std::num::ParseIntError, Box), + // InvalidOctal(std::num::ParseIntError, Box), + // InvalidBinary(std::num::ParseIntError, Box), + // QualifiedPatternIdent(InlinableString), + // CircularDef( + // Vec>, + // Vec<(Region /* pattern */, Region /* expr */)>, + // ), + // + // /// When the author specifies a type annotation but no implementation + // NoImplementation, + todo!("TODO implement run time error reporting for {:?}", other) + } + } +} + +fn not_found<'b>( + alloc: &'b RocDocAllocator<'b>, + region: roc_region::all::Region, + name: &str, + thing: &'b str, + options: MutSet>, +) -> RocDocBuilder<'b> { + use crate::error::r#type::suggest; + + let mut suggestions = suggest::sort(name, options.iter().map(|v| v.as_ref()).collect()); + suggestions.truncate(4); + + let default_no = alloc.concat(vec![ + alloc.reflow("Is there an "), + alloc.keyword("import"), + alloc.reflow(" or "), + alloc.keyword("exposing"), + alloc.reflow(" missing up-top"), + ]); + + let default_yes = alloc.reflow("these names seem close though:"); + + let to_details = |no_suggestion_details, yes_suggestion_details| { + if suggestions.is_empty() { + no_suggestion_details + } else { + alloc.stack(vec![ + yes_suggestion_details, + alloc + .vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string()))) + .indent(4), + ]) + } + }; + + alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("I cannot find a `"), + alloc.string(name.to_string()), + alloc.reflow("` "), + alloc.reflow(thing), + ]), + alloc.region(region), + to_details(default_no, default_yes), + ]) +} diff --git a/compiler/reporting/src/error/mod.rs b/compiler/reporting/src/error/mod.rs new file mode 100644 index 0000000000..2bf7e77288 --- /dev/null +++ b/compiler/reporting/src/error/mod.rs @@ -0,0 +1,4 @@ +pub mod canonicalize; +pub mod mono; +pub mod parse; +pub mod r#type; diff --git a/compiler/reporting/src/error/mono.rs b/compiler/reporting/src/error/mono.rs new file mode 100644 index 0000000000..12f9f54801 --- /dev/null +++ b/compiler/reporting/src/error/mono.rs @@ -0,0 +1,147 @@ +use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder}; +use std::path::PathBuf; +use ven_pretty::DocAllocator; + +pub fn mono_problem<'b>( + alloc: &'b RocDocAllocator<'b>, + filename: PathBuf, + problem: roc_mono::expr::MonoProblem, +) -> Report<'b> { + use roc_mono::expr::MonoProblem::*; + use roc_mono::pattern::Context::*; + use roc_mono::pattern::Error::*; + + match problem { + PatternProblem(Incomplete(region, context, missing)) => match context { + BadArg => { + let doc = alloc.stack(vec![ + alloc.reflow("This pattern does not cover all the possibilities:"), + alloc.region(region), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.concat(vec![ + alloc.reflow( + "I would have to crash if I saw one of those! \ + So rather than pattern matching in function arguments, put a ", + ), + alloc.keyword("when"), + alloc.reflow(" in the function body to account for all possibilities."), + ]), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + } + } + BadDestruct => { + let doc = alloc.stack(vec![ + alloc.reflow("This pattern does not cover all the possibilities:"), + alloc.region(region), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.concat(vec![ + alloc.reflow( + "I would have to crash if I saw one of those! \ + You can use a binding to deconstruct a value if there is only ONE possibility. \ + Use a " + ), + alloc.keyword("when"), + alloc.reflow(" to account for all possibilities."), + ]), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + } + } + BadCase => { + let doc = alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("This "), + alloc.keyword("when"), + alloc.reflow(" does not cover all the possibilities:"), + ]), + alloc.region(region), + alloc.reflow("Other possibilities include:"), + unhandled_patterns_to_doc_block(alloc, missing), + alloc.reflow( + "I would have to crash if I saw one of those! \ + Add branches for them!", + ), + // alloc.hint().append(alloc.reflow("or use a hole.")), + ]); + + Report { + filename, + title: "UNSAFE PATTERN".to_string(), + doc, + } + } + }, + PatternProblem(Redundant { + overall_region, + branch_region, + index, + }) => { + let doc = alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("The "), + alloc.string(index.ordinal()), + alloc.reflow(" pattern is redundant:"), + ]), + alloc.region_with_subregion(overall_region, branch_region), + alloc.reflow( + "Any value of this shape will be handled by \ + a previous pattern, so this one should be removed.", + ), + ]); + + Report { + filename, + title: "REDUNDANT PATTERN".to_string(), + doc, + } + } + } +} + +pub fn unhandled_patterns_to_doc_block<'b>( + alloc: &'b RocDocAllocator<'b>, + patterns: Vec, +) -> RocDocBuilder<'b> { + alloc + .vcat(patterns.into_iter().map(|v| pattern_to_doc(alloc, v))) + .indent(4) + .annotate(Annotation::TypeBlock) +} + +fn pattern_to_doc<'b>( + alloc: &'b RocDocAllocator<'b>, + pattern: roc_mono::pattern::Pattern, +) -> RocDocBuilder<'b> { + use roc_mono::pattern::Literal::*; + use roc_mono::pattern::Pattern::*; + + match pattern { + Anything => alloc.text("_"), + Literal(l) => match l { + Int(i) => alloc.text(i.to_string()), + Bit(true) => alloc.text("True"), + Bit(false) => alloc.text("False"), + Byte(b) => alloc.text(b.to_string()), + Float(f) => alloc.text(f.to_string()), + Str(s) => alloc.string(s.into()), + }, + Ctor(_, tag_name, args) => { + let arg_docs = args.into_iter().map(|v| pattern_to_doc(alloc, v)); + + let docs = std::iter::once(alloc.tag_name(tag_name)).chain(arg_docs); + + alloc.intersperse(docs, alloc.space()) + } + } +} diff --git a/compiler/reporting/src/error/parse.rs b/compiler/reporting/src/error/parse.rs new file mode 100644 index 0000000000..7f0991de66 --- /dev/null +++ b/compiler/reporting/src/error/parse.rs @@ -0,0 +1,42 @@ +use roc_parse::parser::{Fail, FailReason}; +use std::path::PathBuf; + +use crate::report::{Report, RocDocAllocator}; +use ven_pretty::DocAllocator; + +pub fn parse_problem<'b>( + alloc: &'b RocDocAllocator<'b>, + filename: PathBuf, + problem: Fail, +) -> Report<'b> { + use FailReason::*; + + match problem.reason { + ArgumentsBeforeEquals(region) => { + let doc = alloc.stack(vec![ + alloc.reflow("Unexpected tokens in front of the `=` symbol:"), + alloc.region(region), + ]); + + Report { + filename, + doc, + title: "PARSE PROBLEM".to_string(), + } + } + other => { + // + // Unexpected(char, Region), + // OutdentedTooFar, + // ConditionFailed, + // LineTooLong(u32 /* which line was too long */), + // TooManyLines, + // Eof(Region), + // InvalidPattern, + // ReservedKeyword(Region), + // ArgumentsBeforeEquals, + //} + todo!("unhandled parse error: {:?}", other) + } + } +} diff --git a/compiler/reporting/src/type_error.rs b/compiler/reporting/src/error/type.rs similarity index 95% rename from compiler/reporting/src/type_error.rs rename to compiler/reporting/src/error/type.rs index 36db385824..38c29d6c4e 100644 --- a/compiler/reporting/src/type_error.rs +++ b/compiler/reporting/src/error/type.rs @@ -29,9 +29,111 @@ pub fn type_problem<'b>( CircularType(region, symbol, overall_type) => { to_circular_report(alloc, filename, region, symbol, overall_type) } + BadType(type_problem) => { + use roc_types::types::Problem::*; + match type_problem { + BadTypeArguments { + symbol, + region, + type_got, + alias_needs, + } => { + let needed_arguments = if alias_needs == 1 { + alloc.reflow("1 type argument") + } else { + alloc + .text(alias_needs.to_string()) + .append(alloc.reflow(" type arguments")) + }; + + let found_arguments = alloc.text(type_got.to_string()); + + let doc = alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("The "), + alloc.symbol_unqualified(symbol), + alloc.reflow(" alias expects "), + needed_arguments, + alloc.reflow(", but it got "), + found_arguments, + alloc.reflow(" instead:"), + ]), + alloc.region(region), + alloc.reflow("Are there missing parentheses?"), + ]); + + let title = if type_got > alias_needs { + "TOO MANY TYPE ARGUMENTS".to_string() + } else { + "TOO FEW TYPE ARGUMENTS".to_string() + }; + + Report { + filename, + title, + doc, + } + } + CyclicAlias(symbol, region, others) => { + let (doc, title) = cyclic_alias(alloc, symbol, region, others); + + Report { + filename, + title, + doc, + } + } + + other => panic!("unhandled bad type: {:?}", other), + } + } } } +pub fn cyclic_alias<'b>( + alloc: &'b RocDocAllocator<'b>, + symbol: Symbol, + region: roc_region::all::Region, + others: Vec, +) -> (RocDocBuilder<'b>, String) { + let doc = if others.is_empty() { + alloc.stack(vec![ + alloc + .reflow("The ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(" alias is self-recursive in an invalid way:")), + alloc.region(region), + alloc.reflow("Recursion in aliases is only allowed if recursion happens behind a tag."), + ]) + } else { + alloc.stack(vec![ + alloc + .reflow("The ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow(" alias is recursive in an invalid way:")), + alloc.region(region), + alloc + .reflow("The ") + .append(alloc.symbol_unqualified(symbol)) + .append(alloc.reflow( + " alias depends on itself through the following chain of definitions:", + )), + crate::report::cycle( + alloc, + 4, + alloc.symbol_unqualified(symbol), + others + .into_iter() + .map(|other| alloc.symbol_unqualified(other)) + .collect::>(), + ), + alloc.reflow("Recursion in aliases is only allowed if recursion happens behind a tag."), + ]) + }; + + (doc, "CYCLIC ALIAS".to_string()) +} + #[allow(clippy::too_many_arguments)] fn report_mismatch<'b>( alloc: &'b RocDocAllocator<'b>, @@ -1849,7 +1951,7 @@ mod report_text { fs: Vec<(Lowercase, ErrorType)>, ext: TypeExt, ) -> RocDocBuilder<'b> { - use crate::type_error::{ext_to_doc, to_doc}; + use crate::error::r#type::{ext_to_doc, to_doc}; let entry_to_doc = |(name, tipe): (Lowercase, ErrorType)| { ( @@ -2027,9 +2129,9 @@ mod report_text { fn type_problem_to_pretty<'b>( alloc: &'b RocDocAllocator<'b>, - problem: crate::type_error::Problem, + problem: crate::error::r#type::Problem, ) -> RocDocBuilder<'b> { - use crate::type_error::Problem::*; + use crate::error::r#type::Problem::*; match problem { FieldTypo(typo, possibilities) => { diff --git a/compiler/reporting/src/lib.rs b/compiler/reporting/src/lib.rs index 36507c33e1..12ef1d2a39 100644 --- a/compiler/reporting/src/lib.rs +++ b/compiler/reporting/src/lib.rs @@ -11,5 +11,5 @@ // re-enable this when working on performance optimizations than have it block PRs. #![allow(clippy::large_enum_variant)] +pub mod error; pub mod report; -pub mod type_error; diff --git a/compiler/reporting/src/report.rs b/compiler/reporting/src/report.rs index 875bd6dd11..25cb34c2a9 100644 --- a/compiler/reporting/src/report.rs +++ b/compiler/reporting/src/report.rs @@ -1,13 +1,15 @@ -use roc_collections::all::MutSet; use roc_module::ident::Ident; use roc_module::ident::{Lowercase, TagName, Uppercase}; use roc_module::symbol::{Interns, ModuleId, Symbol}; -use roc_problem::can::PrecedenceProblem::BothNonAssociative; -use roc_problem::can::{Problem, RuntimeError}; use std::fmt; use std::path::PathBuf; use ven_pretty::{BoxAllocator, DocAllocator, DocBuilder, Render, RenderAnnotated}; +pub use crate::error::canonicalize::can_problem; +pub use crate::error::mono::mono_problem; +pub use crate::error::parse::parse_problem; +pub use crate::error::r#type::type_problem; + // const IS_WINDOWS: bool = std::env::consts::OS == "windows"; const IS_WINDOWS: bool = false; @@ -17,7 +19,9 @@ const CYCLE_LN: &str = ["| ", "│ "][!IS_WINDOWS as usize]; const CYCLE_MID: &str = ["| |", "│ ↓"][!IS_WINDOWS as usize]; const CYCLE_END: &str = ["+-<---+", "└─────┘"][!IS_WINDOWS as usize]; -fn cycle<'b>( +const GUTTER_BAR: &str = " ┆"; + +pub fn cycle<'b>( alloc: &'b RocDocAllocator<'b>, indent: usize, name: RocDocBuilder<'b>, @@ -142,417 +146,6 @@ pub const UNDERLINE_CODE: &str = "\u{001b}[4m"; pub const RESET_CODE: &str = "\u{001b}[0m"; -pub fn mono_problem<'b>( - alloc: &'b RocDocAllocator<'b>, - filename: PathBuf, - problem: roc_mono::expr::MonoProblem, -) -> Report<'b> { - use roc_mono::expr::MonoProblem::*; - use roc_mono::pattern::Context::*; - use roc_mono::pattern::Error::*; - - match problem { - PatternProblem(Incomplete(region, context, missing)) => match context { - BadArg => { - let doc = alloc.stack(vec![ - alloc.reflow("This pattern does not cover all the possibilities:"), - alloc.region(region), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.concat(vec![ - alloc.reflow( - "I would have to crash if I saw one of those! \ - So rather than pattern matching in function arguments, put a ", - ), - alloc.keyword("when"), - alloc.reflow(" in the function body to account for all possibilities."), - ]), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - } - } - BadDestruct => { - let doc = alloc.stack(vec![ - alloc.reflow("This pattern does not cover all the possibilities:"), - alloc.region(region), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.concat(vec![ - alloc.reflow( - "I would have to crash if I saw one of those! \ - You can use a binding to deconstruct a value if there is only ONE possibility. \ - Use a " - ), - alloc.keyword("when"), - alloc.reflow(" to account for all possibilities."), - ]), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - } - } - BadCase => { - let doc = alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("This "), - alloc.keyword("when"), - alloc.reflow(" does not cover all the possibilities:"), - ]), - alloc.region(region), - alloc.reflow("Other possibilities include:"), - unhandled_patterns_to_doc_block(alloc, missing), - alloc.reflow( - "I would have to crash if I saw one of those! \ - Add branches for them!", - ), - // alloc.hint().append(alloc.reflow("or use a hole.")), - ]); - - Report { - filename, - title: "UNSAFE PATTERN".to_string(), - doc, - } - } - }, - PatternProblem(Redundant { - overall_region, - branch_region, - index, - }) => { - let doc = alloc.stack(vec![ - alloc.concat(vec![ - alloc.reflow("The "), - alloc.string(index.ordinal()), - alloc.reflow(" pattern is redundant:"), - ]), - alloc.region_with_subregion(overall_region, branch_region), - alloc.reflow( - "Any value of this shape will be handled by \ - a previous pattern, so this one should be removed.", - ), - ]); - - Report { - filename, - title: "REDUNDANT PATTERN".to_string(), - doc, - } - } - } -} - -pub fn unhandled_patterns_to_doc_block<'b>( - alloc: &'b RocDocAllocator<'b>, - patterns: Vec, -) -> RocDocBuilder<'b> { - alloc - .vcat(patterns.into_iter().map(|v| pattern_to_doc(alloc, v))) - .indent(4) - .annotate(Annotation::TypeBlock) -} - -fn pattern_to_doc<'b>( - alloc: &'b RocDocAllocator<'b>, - pattern: roc_mono::pattern::Pattern, -) -> RocDocBuilder<'b> { - use roc_mono::pattern::Literal::*; - use roc_mono::pattern::Pattern::*; - // Anything, - // Literal(Literal), - // Ctor(Union, TagName, std::vec::Vec), - match pattern { - Anything => alloc.text("_"), - Literal(l) => match l { - Int(i) => alloc.text(i.to_string()), - Bit(true) => alloc.text("True"), - Bit(false) => alloc.text("False"), - Byte(b) => alloc.text(b.to_string()), - Float(f) => alloc.text(f.to_string()), - Str(s) => alloc.string(s.into()), - }, - Ctor(_, tag_name, args) => { - let arg_docs = args.into_iter().map(|v| pattern_to_doc(alloc, v)); - - let docs = std::iter::once(alloc.tag_name(tag_name)).chain(arg_docs); - - alloc.intersperse(docs, alloc.space()) - } - } -} - -pub fn can_problem<'b>( - alloc: &'b RocDocAllocator<'b>, - filename: PathBuf, - problem: Problem, -) -> Report<'b> { - let doc = match problem { - Problem::UnusedDef(symbol, region) => { - let line = - r#" then remove it so future readers of your code don't wonder why it is there."#; - - alloc.stack(vec![ - alloc - .symbol_unqualified(symbol) - .append(alloc.reflow(" is not used anywhere in your code.")), - alloc.region(region), - alloc - .reflow("If you didn't intend on using ") - .append(alloc.symbol_unqualified(symbol)) - .append(alloc.reflow(line)), - ]) - } - Problem::UnusedImport(module_id, region) => alloc.concat(vec![ - alloc.reflow("Nothing from "), - alloc.module(module_id), - alloc.reflow(" is used in this module."), - alloc.region(region), - alloc.reflow("Since "), - alloc.module(module_id), - alloc.reflow(" isn't used, you don't need to import it."), - ]), - Problem::UnusedArgument(closure_symbol, argument_symbol, region) => { - let line = "\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used."; - - alloc.concat(vec![ - alloc.symbol_unqualified(closure_symbol), - alloc.reflow(" doesn't use "), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow("."), - alloc.region(region), - alloc.reflow("If you don't need "), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(", then you can just remove it. However, if you really do need "), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(" as an argument of "), - alloc.symbol_unqualified(closure_symbol), - alloc.reflow(", prefix it with an underscore, like this: \"_"), - alloc.symbol_unqualified(argument_symbol), - alloc.reflow(line), - ]) - } - Problem::PrecedenceProblem(BothNonAssociative(region, left_bin_op, right_bin_op)) => alloc - .stack(vec![ - if left_bin_op.value == right_bin_op.value { - alloc.concat(vec![ - alloc.reflow("Using more than one "), - alloc.binop(left_bin_op.value), - alloc.reflow(concat!( - " like this requires parentheses,", - " to clarify how things should be grouped.", - )), - ]) - } else { - alloc.concat(vec![ - alloc.reflow("Using "), - alloc.binop(left_bin_op.value), - alloc.reflow(" and "), - alloc.binop(right_bin_op.value), - alloc.reflow(concat!( - " together requires parentheses, ", - "to clarify how they should be grouped." - )), - ]) - }, - alloc.region(region), - ]), - Problem::UnsupportedPattern(pattern_type, region) => { - use roc_parse::pattern::PatternType::*; - - let this_thing = match pattern_type { - TopLevelDef => "a top-level definition:", - DefExpr => "a value definition:", - FunctionArg => "function arguments:", - WhenBranch => unreachable!("all patterns are allowed in a When"), - }; - - let suggestion = vec![ - alloc.reflow( - "Patterns like this don't cover all possible shapes of the input type. Use a ", - ), - alloc.keyword("when"), - alloc.reflow(" ... "), - alloc.keyword("is"), - alloc.reflow(" instead."), - ]; - - alloc.stack(vec![ - alloc - .reflow("This pattern is not allowed in ") - .append(alloc.reflow(this_thing)), - alloc.region(region), - alloc.concat(suggestion), - ]) - } - Problem::ShadowingInAnnotation { - original_region, - shadow, - } => pretty_runtime_error( - alloc, - RuntimeError::Shadowing { - original_region, - shadow, - }, - ), - Problem::RuntimeError(runtime_error) => pretty_runtime_error(alloc, runtime_error), - }; - - Report { - title: "SYNTAX PROBLEM".to_string(), - filename, - doc, - } -} - -fn not_found<'b>( - alloc: &'b RocDocAllocator<'b>, - region: roc_region::all::Region, - name: &str, - thing: &str, - options: MutSet>, -) -> RocDocBuilder<'b> { - use crate::type_error::suggest; - - let mut suggestions = suggest::sort(name, options.iter().map(|v| v.as_ref()).collect()); - suggestions.truncate(4); - - let default_no = alloc.concat(vec![ - alloc.reflow("Is there an "), - alloc.keyword("import"), - alloc.reflow(" or "), - alloc.keyword("exposing"), - alloc.reflow(" missing up-top"), - ]); - - let default_yes = alloc.reflow("these names seem close though:"); - - let to_details = |no_suggestion_details, yes_suggestion_details| { - if suggestions.is_empty() { - no_suggestion_details - } else { - alloc.stack(vec![ - yes_suggestion_details, - alloc - .vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string()))) - .indent(4), - ]) - } - }; - - alloc.stack(vec![ - alloc.string(format!("I cannot find a `{}` {}", name, thing)), - alloc.region(region), - to_details(default_no, default_yes), - ]) -} -fn pretty_runtime_error<'b>( - alloc: &'b RocDocAllocator<'b>, - runtime_error: RuntimeError, -) -> RocDocBuilder<'b> { - match runtime_error { - RuntimeError::Shadowing { - original_region, - shadow, - } => { - let line = r#"Since these variables have the same name, it's easy to use the wrong one on accident. Give one of them a new name."#; - - alloc.stack(vec![ - alloc - .text("The ") - .append(alloc.ident(shadow.value)) - .append(alloc.reflow(" name is first defined here:")), - alloc.region(original_region), - alloc.reflow("But then it's defined a second time here:"), - alloc.region(shadow.region), - alloc.reflow(line), - ]) - } - - RuntimeError::LookupNotInScope(loc_name, options) => { - not_found(alloc, loc_name.region, &loc_name.value, "value", options) - } - RuntimeError::CircularDef(mut idents, regions) => { - let first = idents.remove(0); - - if idents.is_empty() { - alloc - .reflow("The ") - .append(alloc.ident(first.value.clone())) - .append(alloc.reflow( - " value is defined directly in terms of itself, causing an infinite loop.", - )) - // TODO "are you trying to mutate a variable? - // TODO hint? - } else { - alloc.stack(vec![ - alloc - .reflow("The ") - .append(alloc.ident(first.value.clone())) - .append( - alloc.reflow(" definition is causing a very tricky infinite loop:"), - ), - alloc.region(regions[0].0), - alloc - .reflow("The ") - .append(alloc.ident(first.value.clone())) - .append(alloc.reflow( - " value depends on itself through the following chain of definitions:", - )), - cycle( - alloc, - 4, - alloc.ident(first.value), - idents - .into_iter() - .map(|ident| alloc.ident(ident.value)) - .collect::>(), - ), - // TODO hint? - ]) - } - } - other => { - // // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! - // UnsupportedPattern(Region), - // UnrecognizedFunctionName(Located), - // SymbolNotExposed { - // module_name: InlinableString, - // ident: InlinableString, - // region: Region, - // }, - // ModuleNotImported { - // module_name: InlinableString, - // ident: InlinableString, - // region: Region, - // }, - // InvalidPrecedence(PrecedenceProblem, Region), - // MalformedIdentifier(Box, Region), - // MalformedClosure(Region), - // FloatOutsideRange(Box), - // IntOutsideRange(Box), - // InvalidHex(std::num::ParseIntError, Box), - // InvalidOctal(std::num::ParseIntError, Box), - // InvalidBinary(std::num::ParseIntError, Box), - // QualifiedPatternIdent(InlinableString), - // CircularDef( - // Vec>, - // Vec<(Region /* pattern */, Region /* expr */)>, - // ), - // - // /// When the author specifies a type annotation but no implementation - // NoImplementation, - todo!("TODO implement run time error reporting for {:?}", other) - } - } -} - // define custom allocator struct so we can `impl RocDocAllocator` custom helpers pub struct RocDocAllocator<'a> { upstream: BoxAllocator, @@ -711,6 +304,7 @@ impl<'a> RocDocAllocator<'a> { self.text(content.to_string()).annotate(Annotation::BinOp) } + /// Turns of backticks/colors in a block pub fn type_block( &'a self, content: DocBuilder<'a, Self, Annotation>, @@ -724,6 +318,107 @@ impl<'a> RocDocAllocator<'a> { .annotate(Annotation::Hint) } + pub fn region_all_the_things( + &'a self, + region: roc_region::all::Region, + sub_region1: roc_region::all::Region, + sub_region2: roc_region::all::Region, + error_annotation: Annotation, + ) -> DocBuilder<'a, Self, Annotation> { + debug_assert!(region.contains(&sub_region1)); + debug_assert!(region.contains(&sub_region2)); + + // if true, the final line of the snippet will be some ^^^ that point to the region where + // the problem is. Otherwise, the snippet will have a > on the lines that are in the regon + // where the problem is. + let error_highlight_line = region.start_line == region.end_line; + + let max_line_number_length = (region.end_line + 1).to_string().len(); + let indent = 2; + + let mut result = self.nil(); + for i in region.start_line..=region.end_line { + let line_number_string = (i + 1).to_string(); + let line_number = line_number_string; + let this_line_number_length = line_number.len(); + + let line = self.src_lines[i as usize]; + + let rest_of_line = if !line.trim().is_empty() { + self.text(line).indent(indent) + } else { + self.nil() + }; + + let highlight = !error_highlight_line + && ((i >= sub_region1.start_line && i <= sub_region1.end_line) + || (i >= sub_region2.start_line && i <= sub_region2.end_line)); + + let source_line = if highlight { + self.text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) + .append(self.text(">").annotate(error_annotation)) + .append(rest_of_line) + } else if error_highlight_line { + self.text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) + .append(rest_of_line) + } else { + self.text(" ".repeat(max_line_number_length - this_line_number_length)) + .append(self.text(line_number).annotate(Annotation::LineNumber)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) + .append(self.text(" ")) + .append(rest_of_line) + }; + + result = result.append(source_line); + + if i != region.end_line { + result = result.append(self.line()) + } + } + + if error_highlight_line { + let overlapping = sub_region2.start_col < sub_region1.end_col; + + let highlight = if overlapping { + self.text("^".repeat((sub_region2.end_col - sub_region1.start_col) as usize)) + } else { + let highlight1 = "^".repeat((sub_region1.end_col - sub_region1.start_col) as usize); + let highlight2 = if sub_region1 == sub_region2 { + "".repeat(0) + } else { + "^".repeat((sub_region2.end_col - sub_region2.start_col) as usize) + }; + let inbetween = " " + .repeat((sub_region2.start_col.saturating_sub(sub_region1.end_col)) as usize); + + self.text(highlight1) + .append(self.text(inbetween)) + .append(self.text(highlight2)) + }; + + let highlight_line = self + .line() + .append(self.text(" ".repeat(max_line_number_length))) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) + .append(if sub_region1.is_empty() && sub_region2.is_empty() { + self.nil() + } else { + self.text(" ".repeat(sub_region1.start_col as usize)) + .indent(indent) + .append(highlight) + .annotate(error_annotation) + }); + + result = result.append(highlight_line); + } + + result.annotate(Annotation::CodeBlock) + } + pub fn region_with_subregion( &'a self, region: roc_region::all::Region, @@ -731,6 +426,11 @@ impl<'a> RocDocAllocator<'a> { ) -> DocBuilder<'a, Self, Annotation> { debug_assert!(region.contains(&sub_region)); + // If the outer region takes more than 1 full screen (~60 lines), only show the inner region + if region.end_line - region.start_line > 60 { + return self.region_with_subregion(sub_region, sub_region); + } + // if true, the final line of the snippet will be some ^^^ that point to the region where // the problem is. Otherwise, the snippet will have a > on the lines that are in the regon // where the problem is. @@ -761,18 +461,18 @@ impl<'a> RocDocAllocator<'a> { { self.text(" ".repeat(max_line_number_length - this_line_number_length)) .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(" ┆").annotate(Annotation::GutterBar)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) .append(self.text(">").annotate(Annotation::Error)) .append(rest_of_line) } else if error_highlight_line { self.text(" ".repeat(max_line_number_length - this_line_number_length)) .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(" ┆").annotate(Annotation::GutterBar)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) .append(rest_of_line) } else { self.text(" ".repeat(max_line_number_length - this_line_number_length)) .append(self.text(line_number).annotate(Annotation::LineNumber)) - .append(self.text(" ┆").annotate(Annotation::GutterBar)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) .append(self.text(" ")) .append(rest_of_line) }; @@ -789,7 +489,7 @@ impl<'a> RocDocAllocator<'a> { let highlight_line = self .line() .append(self.text(" ".repeat(max_line_number_length))) - .append(self.text(" ┆").annotate(Annotation::GutterBar)) + .append(self.text(GUTTER_BAR).annotate(Annotation::GutterBar)) .append(if highlight_text.is_empty() { self.nil() } else { @@ -808,6 +508,40 @@ impl<'a> RocDocAllocator<'a> { self.region_with_subregion(region, region) } + pub fn region_without_error( + &'a self, + region: roc_region::all::Region, + ) -> DocBuilder<'a, Self, Annotation> { + let mut result = self.nil(); + for i in region.start_line..=region.end_line { + let line = if i == region.start_line { + if i == region.end_line { + &self.src_lines[i as usize][region.start_col as usize..region.end_col as usize] + } else { + &self.src_lines[i as usize][region.start_col as usize..] + } + } else if i == region.end_line { + &self.src_lines[i as usize][0..region.end_col as usize] + } else { + self.src_lines[i as usize] + }; + + let rest_of_line = if !line.trim().is_empty() { + self.text(line).annotate(Annotation::CodeBlock) + } else { + self.nil() + }; + + result = result.append(rest_of_line); + + if i != region.end_line { + result = result.append(self.line()) + } + } + + result.indent(4) + } + pub fn ident(&'a self, ident: Ident) -> DocBuilder<'a, Self, Annotation> { self.text(format!("{}", ident.as_inline_str())) .annotate(Annotation::Symbol) @@ -843,6 +577,7 @@ pub enum Annotation { pub struct CiWrite { style_stack: Vec, in_type_block: bool, + in_code_block: bool, upstream: W, } @@ -851,6 +586,7 @@ impl CiWrite { CiWrite { style_stack: vec![], in_type_block: false, + in_code_block: false, upstream, } } @@ -898,6 +634,9 @@ where TypeBlock => { self.in_type_block = true; } + CodeBlock => { + self.in_code_block = true; + } Emphasized => { self.write_str("*")?; } @@ -906,7 +645,7 @@ where } GlobalTag | PrivateTag | Keyword | RecordField | Symbol | Typo | TypoSuggestion | TypeVariable - if !self.in_type_block => + if !self.in_type_block && !self.in_code_block => { self.write_str("`")?; } @@ -926,6 +665,9 @@ where TypeBlock => { self.in_type_block = false; } + CodeBlock => { + self.in_code_block = false; + } Emphasized => { self.write_str("*")?; } @@ -934,7 +676,7 @@ where } GlobalTag | PrivateTag | Keyword | RecordField | Symbol | Typo | TypoSuggestion | TypeVariable - if !self.in_type_block => + if !self.in_type_block && !self.in_code_block => { self.write_str("`")?; } diff --git a/compiler/reporting/tests/helpers/mod.rs b/compiler/reporting/tests/helpers/mod.rs index 18179a59a1..b48de7eab3 100644 --- a/compiler/reporting/tests/helpers/mod.rs +++ b/compiler/reporting/tests/helpers/mod.rs @@ -1,7 +1,6 @@ extern crate bumpalo; use self::bumpalo::Bump; -use roc_builtins::unique::uniq_stdlib; use roc_can::constraint::Constraint; use roc_can::env::Env; use roc_can::expected::Expected; @@ -11,13 +10,12 @@ use roc_can::scope::Scope; use roc_collections::all::{ImMap, MutMap, SendMap, SendSet}; use roc_constrain::expr::constrain_expr; use roc_constrain::module::{constrain_imported_values, load_builtin_aliases, Import}; -use roc_module::ident::Ident; -use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds, Symbol}; +use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds}; use roc_parse::ast::{self, Attempting}; use roc_parse::blankspace::space0_before; use roc_parse::parser::{loc, Fail, Parser, State}; use roc_problem::can::Problem; -use roc_region::all::{Located, Region}; +use roc_region::all::Located; use roc_solve::solve; use roc_types::subs::{Content, Subs, VarStore, Variable}; use roc_types::types::Type; @@ -103,96 +101,10 @@ pub fn parse_loc_with<'a>(arena: &'a Bump, input: &'a str) -> Result CanExprOut { +pub fn can_expr(expr_str: &str) -> Result { can_expr_with(&Bump::new(), test_home(), expr_str) } -#[allow(dead_code)] -pub fn uniq_expr( - expr_str: &str, -) -> ( - Located, - Output, - Vec, - Subs, - Variable, - Constraint, - ModuleId, - Interns, -) { - let declared_idents: &ImMap = &ImMap::default(); - - uniq_expr_with(&Bump::new(), expr_str, declared_idents) -} - -#[allow(dead_code)] -pub fn uniq_expr_with( - arena: &Bump, - expr_str: &str, - declared_idents: &ImMap, -) -> ( - Located, - Output, - Vec, - Subs, - Variable, - Constraint, - ModuleId, - Interns, -) { - let home = test_home(); - let CanExprOut { - loc_expr, - output, - problems, - var_store: old_var_store, - var, - interns, - .. - } = can_expr_with(arena, home, expr_str); - - // double check - let var_store = VarStore::new(old_var_store.fresh()); - - let expected2 = Expected::NoExpectation(Type::Variable(var)); - let constraint = roc_constrain::uniq::constrain_declaration( - home, - &var_store, - Region::zero(), - &loc_expr, - declared_idents, - expected2, - ); - - let stdlib = uniq_stdlib(); - - let types = stdlib.types; - let imports: Vec<_> = types - .iter() - .map(|(symbol, (solved_type, region))| Import { - loc_symbol: Located::at(*region, *symbol), - solved_type: solved_type, - }) - .collect(); - - // load builtin values - - // TODO what to do with those rigids? - let (_introduced_rigids, constraint) = - constrain_imported_values(imports, constraint, &var_store); - - // load builtin types - let mut constraint = load_builtin_aliases(&stdlib.aliases, constraint, &var_store); - - constraint.instantiate_aliases(&var_store); - - let subs2 = Subs::new(var_store.into()); - - ( - loc_expr, output, problems, subs2, var, constraint, home, interns, - ) -} - pub struct CanExprOut { pub loc_expr: Located, pub output: Output, @@ -204,14 +116,34 @@ pub struct CanExprOut { pub constraint: Constraint, } +#[derive(Debug)] +pub struct ParseErrOut { + pub fail: Fail, + pub home: ModuleId, + pub interns: Interns, +} + #[allow(dead_code)] -pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut { - let loc_expr = parse_loc_with(&arena, expr_str).unwrap_or_else(|e| { - panic!( - "can_expr_with() got a parse error when attempting to canonicalize:\n\n{:?} {:?}", - expr_str, e - ) - }); +pub fn can_expr_with( + arena: &Bump, + home: ModuleId, + expr_str: &str, +) -> Result { + let loc_expr = match parse_loc_with(&arena, expr_str) { + Ok(e) => e, + Err(fail) => { + let interns = Interns { + module_ids: ModuleIds::default(), + all_ident_ids: MutMap::default(), + }; + + return Err(ParseErrOut { + fail, + interns, + home, + }); + } + }; let var_store = VarStore::default(); let var = var_store.fresh(); @@ -283,7 +215,7 @@ pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut all_ident_ids, }; - CanExprOut { + Ok(CanExprOut { loc_expr, output, problems: env.problems, @@ -292,7 +224,7 @@ pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut interns, var, constraint, - } + }) } #[allow(dead_code)] diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index bd42fea495..807c26cc9a 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -14,15 +14,15 @@ mod test_reporting { use roc_module::symbol::{Interns, ModuleId}; use roc_mono::expr::{Expr, Procs}; use roc_reporting::report::{ - can_problem, mono_problem, Report, BLUE_CODE, BOLD_CODE, CYAN_CODE, DEFAULT_PALETTE, - GREEN_CODE, MAGENTA_CODE, RED_CODE, RESET_CODE, UNDERLINE_CODE, WHITE_CODE, YELLOW_CODE, + can_problem, mono_problem, parse_problem, type_problem, Report, BLUE_CODE, BOLD_CODE, + CYAN_CODE, DEFAULT_PALETTE, GREEN_CODE, MAGENTA_CODE, RED_CODE, RESET_CODE, UNDERLINE_CODE, + WHITE_CODE, YELLOW_CODE, }; - use roc_reporting::type_error::type_problem; use roc_types::pretty_print::name_all_type_vars; use roc_types::subs::Subs; use std::path::PathBuf; // use roc_region::all; - use crate::helpers::{can_expr, infer_expr, CanExprOut}; + use crate::helpers::{can_expr, infer_expr, CanExprOut, ParseErrOut}; use roc_reporting::report::{RocDocAllocator, RocDocBuilder}; use roc_solve::solve; @@ -43,13 +43,16 @@ mod test_reporting { fn infer_expr_help( expr_src: &str, - ) -> ( - Vec, - Vec, - Vec, - ModuleId, - Interns, - ) { + ) -> Result< + ( + Vec, + Vec, + Vec, + ModuleId, + Interns, + ), + ParseErrOut, + > { let CanExprOut { loc_expr, output, @@ -60,7 +63,7 @@ mod test_reporting { mut interns, problems: can_problems, .. - } = can_expr(expr_src); + } = can_expr(expr_src)?; let mut subs = Subs::new(var_store.into()); for (var, name) in output.introduced_variables.name_by_var { @@ -99,7 +102,7 @@ mod test_reporting { ); } - (unify_problems, can_problems, mono_problems, home, interns) + Ok((unify_problems, can_problems, mono_problems, home, interns)) } fn list_reports(src: &str, buf: &mut String, callback: F) @@ -108,40 +111,57 @@ mod test_reporting { { use ven_pretty::DocAllocator; - let (type_problems, can_problems, mono_problems, home, interns) = infer_expr_help(src); - let src_lines: Vec<&str> = src.split('\n').collect(); - let alloc = RocDocAllocator::new(&src_lines, home, &interns); let filename = filename_from_string(r"\code\proj\Main.roc"); - let mut reports = Vec::new(); - for problem in can_problems { - let report = can_problem(&alloc, filename.clone(), problem.clone()); - reports.push(report); + match infer_expr_help(src) { + Err(parse_err) => { + let ParseErrOut { + fail, + home, + interns, + } = parse_err; + + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + + let doc = parse_problem(&alloc, filename, fail); + + callback(doc.pretty(&alloc).append(alloc.line()), buf) + } + Ok((type_problems, can_problems, mono_problems, home, interns)) => { + let mut reports = Vec::new(); + + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + + for problem in can_problems { + let report = can_problem(&alloc, filename.clone(), problem.clone()); + reports.push(report); + } + + for problem in type_problems { + let report = type_problem(&alloc, filename.clone(), problem.clone()); + reports.push(report); + } + + for problem in mono_problems { + let report = mono_problem(&alloc, filename.clone(), problem.clone()); + reports.push(report); + } + + let has_reports = !reports.is_empty(); + + let doc = alloc + .stack(reports.into_iter().map(|v| v.pretty(&alloc))) + .append(if has_reports { + alloc.line() + } else { + alloc.nil() + }); + + callback(doc, buf) + } } - - for problem in type_problems { - let report = type_problem(&alloc, filename.clone(), problem.clone()); - reports.push(report); - } - - for problem in mono_problems { - let report = mono_problem(&alloc, filename.clone(), problem.clone()); - reports.push(report); - } - - let has_reports = !reports.is_empty(); - - let doc = alloc - .stack(reports.into_iter().map(|v| v.pretty(&alloc))) - .append(if has_reports { - alloc.line() - } else { - alloc.nil() - }); - - callback(doc, buf) } fn report_problem_as(src: &str, expected_rendering: &str) { @@ -491,7 +511,8 @@ mod test_reporting { "# ); - let (_type_problems, _can_problems, _mono_problems, home, interns) = infer_expr_help(src); + let (_type_problems, _can_problems, _mono_problems, home, interns) = + infer_expr_help(src).expect("parse error"); let mut buf = String::new(); let src_lines: Vec<&str> = src.split('\n').collect(); @@ -521,7 +542,7 @@ mod test_reporting { ); let (_type_problems, _can_problems, _mono_problems, home, mut interns) = - infer_expr_help(src); + infer_expr_help(src).expect("parse error"); let mut buf = String::new(); let src_lines: Vec<&str> = src.split('\n').collect(); @@ -2257,4 +2278,439 @@ mod test_reporting { ), ) } + + #[test] + fn circular_alias() { + report_problem_as( + indoc!( + r#" + Foo : { x: Bar } + Bar : { y : Foo } + + f : Foo + + f + "# + ), + // should not report Bar as unused! + indoc!( + r#" + -- CYCLIC ALIAS ---------------------------------------------------------------- + + The `Bar` alias is recursive in an invalid way: + + 2 ┆ Bar : { y : Foo } + ┆ ^^^^^^^^^^^ + + The `Bar` alias depends on itself through the following chain of + definitions: + + ┌─────┐ + │ Bar + │ ↓ + │ Foo + └─────┘ + + Recursion in aliases is only allowed if recursion happens behind a + tag. + + -- SYNTAX PROBLEM -------------------------------------------------------------- + + `Bar` is not used anywhere in your code. + + 2 ┆ Bar : { y : Foo } + ┆ ^^^^^^^^^^^^^^^^^ + + If you didn't intend on using `Bar` then remove it so future readers of + your code don't wonder why it is there. + "# + ), + ) + } + + #[test] + fn self_recursive_alias() { + report_problem_as( + indoc!( + r#" + Foo : { x : Foo } + + f : Foo + f = 3 + + f + "# + ), + // should not report Bar as unused! + indoc!( + r#" + -- CYCLIC ALIAS ---------------------------------------------------------------- + + The `Foo` alias is self-recursive in an invalid way: + + 1 ┆ Foo : { x : Foo } + ┆ ^^^ + + Recursion in aliases is only allowed if recursion happens behind a + tag. + "# + ), + ) + } + + #[test] + fn record_duplicate_field_same_type() { + report_problem_as( + indoc!( + r#" + { x: 4, y: 3, x: 4 } + "# + ), + indoc!( + r#" + -- SYNTAX PROBLEM -------------------------------------------------------------- + + This record defines the `.x` field twice! + + 1 ┆ { x: 4, y: 3, x: 4 } + ┆ ^^^^ ^^^^ + + In the rest of the program, I will only use the latter definition: + + 1 ┆ { x: 4, y: 3, x: 4 } + ┆ ^^^^ + + For clarity, remove the previous `.x` definitions from this record. + "# + ), + ) + } + + #[test] + fn record_duplicate_field_different_types() { + report_problem_as( + indoc!( + r#" + { x: 4, y: 3, x: "foo" } + "# + ), + indoc!( + r#" + -- SYNTAX PROBLEM -------------------------------------------------------------- + + This record defines the `.x` field twice! + + 1 ┆ { x: 4, y: 3, x: "foo" } + ┆ ^^^^ ^^^^^^^^ + + In the rest of the program, I will only use the latter definition: + + 1 ┆ { x: 4, y: 3, x: "foo" } + ┆ ^^^^^^^^ + + For clarity, remove the previous `.x` definitions from this record. + "# + ), + ) + } + + #[test] + fn record_duplicate_field_multiline() { + report_problem_as( + indoc!( + r#" + { + x: 4, + y: 3, + x: "foo" + } + "# + ), + indoc!( + r#" + -- SYNTAX PROBLEM -------------------------------------------------------------- + + This record defines the `.x` field twice! + + 1 ┆ { + 2 ┆> x: 4, + 3 ┆ y: 3, + 4 ┆> x: "foo" + 5 ┆ } + + In the rest of the program, I will only use the latter definition: + + 1 ┆ { + 2 ┆ x: 4, + 3 ┆ y: 3, + 4 ┆> x: "foo" + 5 ┆ } + + For clarity, remove the previous `.x` definitions from this record. + "# + ), + ) + } + + #[test] + fn record_update_duplicate_field_multiline() { + report_problem_as( + indoc!( + r#" + \r -> + { r & + x: 4, + y: 3, + x: "foo" + } + "# + ), + indoc!( + r#" + -- SYNTAX PROBLEM -------------------------------------------------------------- + + This record defines the `.x` field twice! + + 2 ┆ { r & + 3 ┆> x: 4, + 4 ┆ y: 3, + 5 ┆> x: "foo" + 6 ┆ } + + In the rest of the program, I will only use the latter definition: + + 2 ┆ { r & + 3 ┆ x: 4, + 4 ┆ y: 3, + 5 ┆> x: "foo" + 6 ┆ } + + For clarity, remove the previous `.x` definitions from this record. + "# + ), + ) + } + + #[test] + fn record_type_duplicate_field() { + report_problem_as( + indoc!( + r#" + a : { foo : Int, bar : Float, foo : Str } + a = { bar: 3.0, foo: "foo" } + + a + "# + ), + indoc!( + r#" + -- SYNTAX PROBLEM -------------------------------------------------------------- + + This record type defines the `.foo` field twice! + + 1 ┆ a : { foo : Int, bar : Float, foo : Str } + ┆ ^^^^^^^^^ ^^^^^^^^^ + + In the rest of the program, I will only use the latter definition: + + 1 ┆ a : { foo : Int, bar : Float, foo : Str } + ┆ ^^^^^^^^^ + + For clarity, remove the previous `.foo` definitions from this record + type. + "# + ), + ) + } + + #[test] + fn tag_union_duplicate_tag() { + report_problem_as( + indoc!( + r#" + a : [ Foo Int, Bar Float, Foo Str ] + a = Foo "foo" + + a + "# + ), + indoc!( + r#" + -- SYNTAX PROBLEM -------------------------------------------------------------- + + This tag union type defines the `Foo` tag twice! + + 1 ┆ a : [ Foo Int, Bar Float, Foo Str ] + ┆ ^^^^^^^ ^^^^^^^ + + In the rest of the program, I will only use the latter definition: + + 1 ┆ a : [ Foo Int, Bar Float, Foo Str ] + ┆ ^^^^^^^ + + For clarity, remove the previous `Foo` definitions from this tag union + type. + "# + ), + ) + } + + #[test] + fn invalid_num() { + report_problem_as( + indoc!( + r#" + a : Num Int Float + a = 3 + + a + "# + ), + indoc!( + r#" + -- TOO MANY TYPE ARGUMENTS ----------------------------------------------------- + + The `Num` alias expects 1 type argument, but it got 2 instead: + + 1 ┆ a : Num Int Float + ┆ ^^^^^^^^^^^^^ + + Are there missing parentheses? + "# + ), + ) + } + + #[test] + fn invalid_num_fn() { + report_problem_as( + indoc!( + r#" + f : Bool -> Num Int Float + f = \_ -> 3 + + f + "# + ), + indoc!( + r#" + -- TOO MANY TYPE ARGUMENTS ----------------------------------------------------- + + The `Num` alias expects 1 type argument, but it got 2 instead: + + 1 ┆ f : Bool -> Num Int Float + ┆ ^^^^^^^^^^^^^ + + Are there missing parentheses? + "# + ), + ) + } + + #[test] + fn too_few_type_arguments() { + report_problem_as( + indoc!( + r#" + Pair a b : [ Pair a b ] + + x : Pair Int + x = 3 + + x + "# + ), + indoc!( + r#" + -- TOO FEW TYPE ARGUMENTS ------------------------------------------------------ + + The `Pair` alias expects 2 type arguments, but it got 1 instead: + + 3 ┆ x : Pair Int + ┆ ^^^^^^^^ + + Are there missing parentheses? + "# + ), + ) + } + + #[test] + fn too_many_type_arguments() { + report_problem_as( + indoc!( + r#" + Pair a b : [ Pair a b ] + + x : Pair Int Int Int + x = 3 + + x + "# + ), + indoc!( + r#" + -- TOO MANY TYPE ARGUMENTS ----------------------------------------------------- + + The `Pair` alias expects 2 type arguments, but it got 3 instead: + + 3 ┆ x : Pair Int Int Int + ┆ ^^^^^^^^^^^^^^^^ + + Are there missing parentheses? + "# + ), + ) + } + + #[test] + fn phantom_type_variable() { + report_problem_as( + indoc!( + r#" + Foo a : [ Foo ] + + f : Foo Int + + f + "# + ), + indoc!( + r#" + -- SYNTAX PROBLEM -------------------------------------------------------------- + + The `a` type variable is not used in the `Foo` alias definition: + + 1 ┆ Foo a : [ Foo ] + ┆ ^ + + Roc does not allow unused type parameters! + + Hint: If you want an unused type parameter (a so-called "phantom + type"), read the guide section on phantom data. + "# + ), + ) + } + + #[test] + fn elm_function_syntax() { + report_problem_as( + indoc!( + r#" + f x y = x + "# + ), + indoc!( + r#" + -- PARSE PROBLEM --------------------------------------------------------------- + + Unexpected tokens in front of the `=` symbol: + + 1 ┆ f x y = x + ┆ ^^^ + "# + ), + ) + } } diff --git a/compiler/solve/Cargo.toml b/compiler/solve/Cargo.toml index 74af9875cc..1294022137 100644 --- a/compiler/solve/Cargo.toml +++ b/compiler/solve/Cargo.toml @@ -18,7 +18,7 @@ roc_builtins = { path = "../builtins" } roc_problem = { path = "../problem" } roc_parse = { path = "../parse" } roc_solve = { path = "../solve" } -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/solve/src/solve.rs b/compiler/solve/src/solve.rs index 29321200d8..488fdaa912 100644 --- a/compiler/solve/src/solve.rs +++ b/compiler/solve/src/solve.rs @@ -21,6 +21,7 @@ pub enum TypeError { BadExpr(Region, Category, ErrorType, Expected), BadPattern(Region, PatternCategory, ErrorType, PExpected), CircularType(Region, Symbol, ErrorType), + BadType(roc_types::types::Problem), } pub type SubsByModule = MutMap; @@ -166,6 +167,13 @@ fn solve( problems.push(problem); + state + } + BadType(vars, problem) => { + introduce(subs, rank, pools, &vars); + + problems.push(TypeError::BadType(problem)); + state } } @@ -228,6 +236,13 @@ fn solve( problems.push(problem); + state + } + BadType(vars, problem) => { + introduce(subs, rank, pools, &vars); + + problems.push(TypeError::BadType(problem)); + state } } @@ -278,6 +293,13 @@ fn solve( problems.push(problem); + state + } + BadType(vars, problem) => { + introduce(subs, rank, pools, &vars); + + problems.push(TypeError::BadType(problem)); + state } } @@ -829,7 +851,7 @@ fn circular_error( loc_var: &Located, ) { let var = loc_var.value; - let error_type = subs.var_to_error_type(var); + let (error_type, _) = subs.var_to_error_type(var); let problem = TypeError::CircularType(loc_var.region, symbol, error_type); subs.set_content(var, Content::Error); diff --git a/compiler/types/Cargo.toml b/compiler/types/Cargo.toml index 7efc4c7784..f91a465486 100644 --- a/compiler/types/Cargo.toml +++ b/compiler/types/Cargo.toml @@ -14,7 +14,7 @@ ven_ena = { path = "../../vendor/ena" } inlinable_string = "0.1.0" [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/types/src/subs.rs b/compiler/types/src/subs.rs index c2af5b4d04..1ac2d64b4b 100644 --- a/compiler/types/src/subs.rs +++ b/compiler/types/src/subs.rs @@ -37,9 +37,10 @@ impl fmt::Debug for Mark { } #[derive(Default)] -struct NameState { +struct ErrorTypeState { taken: MutSet, normals: u32, + problems: Vec, } #[derive(Default, Clone)] @@ -363,7 +364,7 @@ impl Subs { explicit_substitute(self, x, y, z, &mut seen) } - pub fn var_to_error_type(&mut self, var: Variable) -> ErrorType { + pub fn var_to_error_type(&mut self, var: Variable) -> (ErrorType, Vec) { let names = get_var_names(self, var, ImMap::default()); let mut taken = MutSet::default(); @@ -371,9 +372,13 @@ impl Subs { taken.insert(name); } - let mut state = NameState { taken, normals: 0 }; + let mut state = ErrorTypeState { + taken, + normals: 0, + problems: Vec::new(), + }; - var_to_err_type(self, &mut state, var) + (var_to_err_type(self, &mut state, var), state.problems) } pub fn restore(&mut self, var: Variable) { @@ -1114,7 +1119,7 @@ where } } -fn var_to_err_type(subs: &mut Subs, state: &mut NameState, var: Variable) -> ErrorType { +fn var_to_err_type(subs: &mut Subs, state: &mut ErrorTypeState, var: Variable) -> ErrorType { let desc = subs.get(var); if desc.mark == Mark::OCCURS { @@ -1132,7 +1137,7 @@ fn var_to_err_type(subs: &mut Subs, state: &mut NameState, var: Variable) -> Err fn content_to_err_type( subs: &mut Subs, - state: &mut NameState, + state: &mut ErrorTypeState, var: Variable, content: Content, ) -> ErrorType { @@ -1174,7 +1179,11 @@ fn content_to_err_type( } } -fn flat_type_to_err_type(subs: &mut Subs, state: &mut NameState, flat_type: FlatType) -> ErrorType { +fn flat_type_to_err_type( + subs: &mut Subs, + state: &mut ErrorTypeState, + flat_type: FlatType, +) -> ErrorType { use self::FlatType::*; match flat_type { @@ -1296,11 +1305,15 @@ fn flat_type_to_err_type(subs: &mut Subs, state: &mut NameState, flat_type: Flat Boolean(b) => ErrorType::Boolean(b), - Erroneous(_) => ErrorType::Error, + Erroneous(problem) => { + state.problems.push(problem); + + ErrorType::Error + } } } -fn get_fresh_var_name(state: &mut NameState) -> Lowercase { +fn get_fresh_var_name(state: &mut ErrorTypeState) -> Lowercase { let (name, new_index) = name_type_var(state.normals, &mut state.taken); state.normals = new_index; diff --git a/compiler/types/src/types.rs b/compiler/types/src/types.rs index 0c6db6cc57..f83d055e29 100644 --- a/compiler/types/src/types.rs +++ b/compiler/types/src/types.rs @@ -395,6 +395,7 @@ impl Type { pub fn instantiate_aliases( &mut self, + region: Region, aliases: &ImMap, var_store: &VarStore, introduced: &mut ImSet, @@ -404,34 +405,44 @@ impl Type { match self { Function(args, ret) => { for arg in args { - arg.instantiate_aliases(aliases, var_store, introduced); + arg.instantiate_aliases(region, aliases, var_store, introduced); } - ret.instantiate_aliases(aliases, var_store, introduced); + ret.instantiate_aliases(region, aliases, var_store, introduced); } RecursiveTagUnion(_, tags, ext) | TagUnion(tags, ext) => { for (_, args) in tags { for x in args { - x.instantiate_aliases(aliases, var_store, introduced); + x.instantiate_aliases(region, aliases, var_store, introduced); } } - ext.instantiate_aliases(aliases, var_store, introduced); + ext.instantiate_aliases(region, aliases, var_store, introduced); } Record(fields, ext) => { for x in fields.iter_mut() { - x.instantiate_aliases(aliases, var_store, introduced); + x.instantiate_aliases(region, aliases, var_store, introduced); } - ext.instantiate_aliases(aliases, var_store, introduced); + ext.instantiate_aliases(region, aliases, var_store, introduced); } Alias(_, type_args, actual_type) => { for arg in type_args { - arg.1.instantiate_aliases(aliases, var_store, introduced); + arg.1 + .instantiate_aliases(region, aliases, var_store, introduced); } - actual_type.instantiate_aliases(aliases, var_store, introduced); + actual_type.instantiate_aliases(region, aliases, var_store, introduced); } Apply(symbol, args) => { if let Some(alias) = aliases.get(symbol) { - debug_assert!(args.len() == alias.vars.len()); + if args.len() != alias.vars.len() { + *self = Type::Erroneous(Problem::BadTypeArguments { + symbol: *symbol, + region, + type_got: args.len() as u8, + alias_needs: alias.vars.len() as u8, + }); + return; + } + let mut actual = alias.typ.clone(); let mut named_args = Vec::with_capacity(args.len()); @@ -447,7 +458,7 @@ impl Type { ) in alias.vars.iter().zip(args.iter()) { let mut filler = filler.clone(); - filler.instantiate_aliases(aliases, var_store, introduced); + filler.instantiate_aliases(region, aliases, var_store, introduced); named_args.push((lowercase.clone(), filler.clone())); substitution.insert(*placeholder, filler); } @@ -463,7 +474,7 @@ impl Type { } actual.substitute(&substitution); - actual.instantiate_aliases(aliases, var_store, introduced); + actual.instantiate_aliases(region, aliases, var_store, introduced); // instantiate recursion variable! if let Type::RecursiveTagUnion(rec_var, mut tags, mut ext) = actual { @@ -487,7 +498,7 @@ impl Type { } else { // one of the special-cased Apply types. for x in args { - x.instantiate_aliases(aliases, var_store, introduced); + x.instantiate_aliases(region, aliases, var_store, introduced); } } } @@ -691,11 +702,18 @@ pub struct Alias { #[derive(PartialEq, Eq, Debug, Clone)] pub enum Problem { CanonicalizationProblem, - Mismatch(Mismatch, ErrorType, ErrorType), CircularType(Symbol, ErrorType, Region), + CyclicAlias(Symbol, Region, Vec), UnrecognizedIdent(InlinableString), Shadowed(Region, Located), + BadTypeArguments { + symbol: Symbol, + region: Region, + type_got: u8, + alias_needs: u8, + }, InvalidModule, + SolvedTypeError, } #[derive(PartialEq, Eq, Debug, Clone)] diff --git a/compiler/unify/Cargo.toml b/compiler/unify/Cargo.toml index 9eb34a086c..31d2d09e68 100644 --- a/compiler/unify/Cargo.toml +++ b/compiler/unify/Cargo.toml @@ -10,7 +10,7 @@ roc_module = { path = "../module" } roc_types = { path = "../types" } [dev-dependencies] -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/compiler/unify/src/unify.rs b/compiler/unify/src/unify.rs index e0d1b25002..d7157557e9 100644 --- a/compiler/unify/src/unify.rs +++ b/compiler/unify/src/unify.rs @@ -73,6 +73,7 @@ struct Context { pub enum Unified { Success(Pool), Failure(Pool, ErrorType, ErrorType), + BadType(Pool, roc_types::types::Problem), } #[derive(Debug)] @@ -91,11 +92,18 @@ pub fn unify(subs: &mut Subs, var1: Variable, var2: Variable) -> Unified { if mismatches.is_empty() { Unified::Success(vars) } else { - let type1 = subs.var_to_error_type(var1); - let type2 = subs.var_to_error_type(var2); + let (type1, mut problems) = subs.var_to_error_type(var1); + let (type2, problems2) = subs.var_to_error_type(var2); + + problems.extend(problems2); subs.union(var1, var2, Content::Error.into()); - Unified::Failure(vars, type1, type2) + + if !problems.is_empty() { + Unified::BadType(vars, problems.remove(0)) + } else { + Unified::Failure(vars, type1, type2) + } } } diff --git a/compiler/uniq/Cargo.toml b/compiler/uniq/Cargo.toml index 6465db915f..8778089d65 100644 --- a/compiler/uniq/Cargo.toml +++ b/compiler/uniq/Cargo.toml @@ -19,7 +19,7 @@ roc_builtins = { path = "../builtins" } roc_problem = { path = "../problem" } roc_parse = { path = "../parse" } roc_solve = { path = "../solve" } -pretty_assertions = "0.5.1 " +pretty_assertions = "0.5.1" maplit = "1.0.1" indoc = "0.3.3" quickcheck = "0.8" diff --git a/examples/hello-world/.gitignore b/examples/.gitignore similarity index 100% rename from examples/hello-world/.gitignore rename to examples/.gitignore diff --git a/examples/hello-world/Hello.roc b/examples/hello-world/Hello.roc new file mode 100644 index 0000000000..ef6ba9f212 --- /dev/null +++ b/examples/hello-world/Hello.roc @@ -0,0 +1,3 @@ +app Hello provides [ main ] imports [] + +main = "Hello, World!" diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md index d53e6e4251..e230651432 100644 --- a/examples/hello-world/README.md +++ b/examples/hello-world/README.md @@ -3,13 +3,13 @@ To run: ```bash -$ cargo run hello.roc +$ cargo run run Hello.roc ``` To run in release mode instead, do: ```bash -$ cargo run --release hello.roc +$ cargo run --release run Hello.roc ``` ## Design Notes diff --git a/examples/hello-world/hello.roc b/examples/hello-world/hello.roc deleted file mode 100644 index b0d555865b..0000000000 --- a/examples/hello-world/hello.roc +++ /dev/null @@ -1 +0,0 @@ -"Hello, World!" diff --git a/examples/hello-world/host.rs b/examples/hello-world/host.rs index eb3702b810..c22b73a49e 100644 --- a/examples/hello-world/host.rs +++ b/examples/hello-world/host.rs @@ -3,7 +3,7 @@ use std::os::raw::c_char; #[link(name = "roc_app", kind = "static")] extern "C" { - #[link_name = "$Test.main"] + #[link_name = "$main"] fn str_from_roc() -> *const c_char; } diff --git a/examples/quicksort/.gitignore b/examples/quicksort/.gitignore deleted file mode 100644 index 0bd05ae96a..0000000000 --- a/examples/quicksort/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -app -*.o -*.a diff --git a/examples/quicksort/Quicksort.roc b/examples/quicksort/Quicksort.roc new file mode 100644 index 0000000000..daba359584 --- /dev/null +++ b/examples/quicksort/Quicksort.roc @@ -0,0 +1,55 @@ +app Quicksort provides [ main ] imports [] + +main = quicksort [ 7, 19, 4, 21 ] + +quicksort : List (Num a) -> List (Num a) +quicksort = \list -> + quicksortHelp list 0 (List.len list - 1) + + +quicksortHelp : List (Num a), Int, Int -> List (Num a) +quicksortHelp = \list, low, high -> + when partition low high list is + Pair partitionIndex partitioned -> + partitioned + |> quicksortHelp low (partitionIndex - 1) + |> quicksortHelp (partitionIndex + 1) high + + +swap : Int, Int, List a -> List a +swap = \i, j, list -> + when Pair (List.get list i) (List.get list j) is + Pair (Ok atI) (Ok atJ) -> + list + |> List.set i atJ + |> List.set j atI + + _ -> + [] + + +partition : Int, Int, List (Num a) -> [ Pair Int (List (Num a)) ] +partition = \low, high, initialList -> + when List.get initialList high is + Ok pivot -> + go = \i, j, list -> + if j < high then + when List.get list j is + Ok value -> + if value <= pivot then + go (i + 1) (j + 1) (swap (i + 1) j list) + else + go i (j + 1) list + + Err _ -> + Pair i list + else + Pair i list + + when go (low - 1) low initialList is + Pair newI newList -> + Pair (newI + 1) (swap (newI + 1) high newList) + + Err _ -> + Pair (low - 1) initialList + diff --git a/examples/quicksort/README.md b/examples/quicksort/README.md index 859f54dfcc..695e1b0605 100644 --- a/examples/quicksort/README.md +++ b/examples/quicksort/README.md @@ -3,11 +3,11 @@ To run: ```bash -$ cargo run qs.roc +$ cargo run run Quicksort.roc ``` To run in release mode instead, do: ```bash -$ cargo run --release qs.roc +$ cargo run --release run Quicksort.roc ```