mirror of
https://github.com/roc-lang/roc.git
synced 2024-09-11 18:58:24 +03:00
Merge remote-tracking branch 'origin/main' into remove-nat
This commit is contained in:
commit
24a38c4a26
@ -10,7 +10,7 @@ On MacOS and Linux, we highly recommend Using [nix](https://nixos.org/download.h
|
||||
|
||||
### On Linux x86_64 or MacOS aarch64/arm64/x86_64
|
||||
|
||||
#### Install
|
||||
#### Installing Nix
|
||||
|
||||
If you are running ArchLinux or a derivative like Manjaro, you'll need to run `sudo sysctl -w kernel.unprivileged_userns_clone=1` before installing nix.
|
||||
|
||||
|
119
Cargo.lock
generated
119
Cargo.lock
generated
@ -771,6 +771,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dissimilar"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632"
|
||||
|
||||
[[package]]
|
||||
name = "distance"
|
||||
version = "0.4.0"
|
||||
@ -838,6 +844,19 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"is-terminal",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@ -864,6 +883,16 @@ dependencies = [
|
||||
"str-buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "expect-test"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3"
|
||||
dependencies = [
|
||||
"dissimilar",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fd-lock"
|
||||
version = "3.0.13"
|
||||
@ -1151,6 +1180,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.27"
|
||||
@ -1327,6 +1362,17 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.3",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.9.0"
|
||||
@ -1922,7 +1968,7 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"env_logger 0.8.4",
|
||||
"log",
|
||||
"rand",
|
||||
]
|
||||
@ -2273,6 +2319,7 @@ dependencies = [
|
||||
"roc_collections",
|
||||
"roc_command_utils",
|
||||
"roc_constrain",
|
||||
"roc_debug_flags",
|
||||
"roc_error_macros",
|
||||
"roc_gen_dev",
|
||||
"roc_gen_llvm",
|
||||
@ -2649,6 +2696,10 @@ name = "roc_lang_srv"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"env_logger 0.10.2",
|
||||
"expect-test",
|
||||
"indoc",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"roc_can",
|
||||
"roc_collections",
|
||||
@ -4546,6 +4597,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
@ -4576,6 +4636,21 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.0",
|
||||
"windows_aarch64_msvc 0.52.0",
|
||||
"windows_i686_gnu 0.52.0",
|
||||
"windows_i686_msvc 0.52.0",
|
||||
"windows_x86_64_gnu 0.52.0",
|
||||
"windows_x86_64_gnullvm 0.52.0",
|
||||
"windows_x86_64_msvc 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@ -4588,6 +4663,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@ -4600,6 +4681,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@ -4612,6 +4699,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@ -4624,6 +4717,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@ -4636,6 +4735,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@ -4648,6 +4753,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@ -4660,6 +4771,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
@ -32,7 +32,7 @@ install-zig-llvm:
|
||||
RUN apt -y install libpolly-16-dev # required by llvm-sys crate
|
||||
ENV RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=native"
|
||||
RUN apt -y install libssl-dev
|
||||
RUN OPENSSL_NO_VENDOR=1 cargo install wasm-pack
|
||||
RUN wget https://rustwasm.github.io/wasm-pack/installer/init.sh -O init.sh && sh init.sh
|
||||
# sccache
|
||||
RUN cargo install sccache --locked
|
||||
RUN sccache -V
|
||||
|
@ -33,6 +33,7 @@ If you would like your company to become a corporate sponsor of Roc's developmen
|
||||
|
||||
We'd also like to express our gratitude to our generous [individual sponsors](https://github.com/sponsors/roc-lang/)! A special thanks to those sponsoring $25/month or more:
|
||||
|
||||
* [Steven Chen](https://github.com/megakilo)
|
||||
* [Drew Lazzeri](https://github.com/asteroidb612)
|
||||
* [Alex Binaei](https://github.com/mrmizz)
|
||||
* [Jono Mallanyk](https://github.com/jonomallanyk)
|
||||
|
@ -553,9 +553,11 @@ mod cli_run {
|
||||
&[],
|
||||
indoc!(
|
||||
r#"
|
||||
── EXPECT FAILED in ...roc/roc/crates/cli_testing_examples/expects/expects.roc ─
|
||||
|
||||
This expectation failed:
|
||||
|
||||
19│ expect words == []
|
||||
28│ expect words == []
|
||||
^^^^^^^^^^^
|
||||
|
||||
When it failed, these variables had these values:
|
||||
@ -563,12 +565,12 @@ mod cli_run {
|
||||
words : List Str
|
||||
words = ["this", "will", "for", "sure", "be", "a", "large", "string", "so", "when", "we", "split", "it", "it", "will", "use", "seamless", "slices", "which", "affect", "printing"]
|
||||
|
||||
[<ignored for tests>:22] x = 42
|
||||
[<ignored for tests>:23] "Fjoer en ferdjer frieten oan dyn geve lea" = "Fjoer en ferdjer frieten oan dyn geve lea"
|
||||
[<ignored for tests>:24] "this is line 24" = "this is line 24"
|
||||
[<ignored for tests>:13] x = "abc"
|
||||
[<ignored for tests>:13] x = 10
|
||||
[<ignored for tests>:13] x = (A (B C))
|
||||
[<ignored for tests>:31] x = 42
|
||||
[<ignored for tests>:33] "Fjoer en ferdjer frieten oan dyn geve lea" = "Fjoer en ferdjer frieten oan dyn geve lea"
|
||||
[<ignored for tests>:35] "this is line 24" = "this is line 24"
|
||||
[<ignored for tests>:21] x = "abc"
|
||||
[<ignored for tests>:21] x = 10
|
||||
[<ignored for tests>:21] x = (A (B C))
|
||||
Program finished!
|
||||
"#
|
||||
),
|
||||
@ -584,20 +586,46 @@ mod cli_run {
|
||||
&[],
|
||||
indoc!(
|
||||
r#"
|
||||
── EXPECT FAILED in ...roc/roc/crates/cli_testing_examples/expects/expects.roc ─
|
||||
|
||||
This expectation failed:
|
||||
|
||||
6│> expect
|
||||
7│> a = 1
|
||||
8│> b = 2
|
||||
9│>
|
||||
10│> a == b
|
||||
9│ expect a == 2
|
||||
^^^^^^
|
||||
|
||||
When it failed, these variables had these values:
|
||||
|
||||
a : Num *
|
||||
a = 1
|
||||
|
||||
b : Num *
|
||||
── EXPECT FAILED in ...roc/roc/crates/cli_testing_examples/expects/expects.roc ─
|
||||
|
||||
This expectation failed:
|
||||
|
||||
10│ expect a == 3
|
||||
^^^^^^
|
||||
|
||||
When it failed, these variables had these values:
|
||||
|
||||
a : Num *
|
||||
a = 1
|
||||
|
||||
── EXPECT FAILED in ...roc/roc/crates/cli_testing_examples/expects/expects.roc ─
|
||||
|
||||
This expectation failed:
|
||||
|
||||
14│> expect
|
||||
15│> a = makeA
|
||||
16│> b = 2i64
|
||||
17│>
|
||||
18│> a == b
|
||||
|
||||
When it failed, these variables had these values:
|
||||
|
||||
a : Int Signed64
|
||||
a = 1
|
||||
|
||||
b : I64
|
||||
b = 2
|
||||
|
||||
|
||||
|
2
crates/cli/tests/fixtures/packages/app.roc
vendored
2
crates/cli/tests/fixtures/packages/app.roc
vendored
@ -3,4 +3,4 @@ app "packages-test"
|
||||
imports [json.JsonParser, csv.Csv]
|
||||
provides [main] to pf
|
||||
|
||||
main = "Hello, World! \(JsonParser.example) \(Csv.example)"
|
||||
main = "Hello, World! $(JsonParser.example) $(Csv.example)"
|
||||
|
@ -10,7 +10,7 @@ show = \list ->
|
||||
|> List.map Num.toStr
|
||||
|> Str.joinWith ", "
|
||||
|
||||
"[\(content)]"
|
||||
"[$(content)]"
|
||||
|
||||
sortBy : List a, (a -> Num *) -> List a
|
||||
sortBy = \list, toComparable ->
|
||||
|
@ -26,7 +26,7 @@ showRBTree = \tree, showKey, showValue ->
|
||||
sL = nodeInParens left showKey showValue
|
||||
sR = nodeInParens right showKey showValue
|
||||
|
||||
"Node \(sColor) \(sKey) \(sValue) \(sL) \(sR)"
|
||||
"Node $(sColor) $(sKey) $(sValue) $(sL) $(sR)"
|
||||
|
||||
nodeInParens : RedBlackTree k v, (k -> Str), (v -> Str) -> Str
|
||||
nodeInParens = \tree, showKey, showValue ->
|
||||
@ -37,7 +37,7 @@ nodeInParens = \tree, showKey, showValue ->
|
||||
Node _ _ _ _ _ ->
|
||||
inner = showRBTree tree showKey showValue
|
||||
|
||||
"(\(inner))"
|
||||
"($(inner))"
|
||||
|
||||
showColor : NodeColor -> Str
|
||||
showColor = \color ->
|
||||
|
@ -14,7 +14,7 @@ main =
|
||||
#
|
||||
# _ ->
|
||||
# ns = Num.toStr n
|
||||
# Task.putLine "No test \(ns)"
|
||||
# Task.putLine "No test $(ns)"
|
||||
showBool : Bool -> Str
|
||||
showBool = \b ->
|
||||
if
|
||||
|
@ -3,14 +3,23 @@ app "expects-test"
|
||||
imports []
|
||||
provides [main] to pf
|
||||
|
||||
expect
|
||||
makeA =
|
||||
a = 1
|
||||
b = 2
|
||||
|
||||
expect a == 2
|
||||
expect a == 3
|
||||
|
||||
a
|
||||
|
||||
expect
|
||||
a = makeA
|
||||
b = 2i64
|
||||
|
||||
a == b
|
||||
|
||||
polyDbg = \x ->
|
||||
dbg x
|
||||
|
||||
x
|
||||
|
||||
main =
|
||||
@ -20,10 +29,12 @@ main =
|
||||
|
||||
x = 42
|
||||
dbg x
|
||||
|
||||
dbg "Fjoer en ferdjer frieten oan dyn geve lea"
|
||||
|
||||
dbg "this is line 24"
|
||||
|
||||
r = {x : polyDbg "abc", y: polyDbg 10u8, z : polyDbg (A (B C))}
|
||||
r = { x: polyDbg "abc", y: polyDbg 10u8, z: polyDbg (A (B C)) }
|
||||
|
||||
when r is
|
||||
_ -> "Program finished!\n"
|
||||
|
@ -12,6 +12,7 @@ roc_bitcode = { path = "../builtins/bitcode" }
|
||||
roc_can = { path = "../can" }
|
||||
roc_collections = { path = "../collections" }
|
||||
roc_constrain = { path = "../constrain" }
|
||||
roc_debug_flags = { path = "../debug_flags" }
|
||||
roc_error_macros = { path = "../../error_macros" }
|
||||
roc_gen_dev = { path = "../gen_dev", default-features = false }
|
||||
roc_gen_llvm = { path = "../gen_llvm" }
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::target::{arch_str, target_zig_str};
|
||||
use libloading::{Error, Library};
|
||||
use roc_command_utils::{cargo, clang, rustup, zig};
|
||||
use roc_debug_flags;
|
||||
use roc_error_macros::internal_error;
|
||||
use roc_mono::ir::OptLevel;
|
||||
use std::collections::HashMap;
|
||||
@ -613,7 +614,8 @@ pub fn rebuild_host(
|
||||
|
||||
// Clean up c_host.o
|
||||
if c_host_dest.exists() {
|
||||
std::fs::remove_file(c_host_dest).unwrap();
|
||||
// there can be a race condition on this file cleanup
|
||||
let _ = std::fs::remove_file(c_host_dest);
|
||||
}
|
||||
}
|
||||
} else if rust_host_src.exists() {
|
||||
@ -1066,6 +1068,7 @@ fn link_linux(
|
||||
"-o",
|
||||
output_path.as_path().to_str().unwrap(), // app (or app.so or app.dylib etc.)
|
||||
]);
|
||||
debug_print_command(&command);
|
||||
|
||||
let output = command.spawn()?;
|
||||
|
||||
@ -1163,14 +1166,18 @@ fn link_macos(
|
||||
output_path.to_str().unwrap(), // app
|
||||
]);
|
||||
|
||||
debug_print_command(&ld_command);
|
||||
|
||||
let mut ld_child = ld_command.spawn()?;
|
||||
|
||||
match target.architecture {
|
||||
Architecture::Aarch64(_) => {
|
||||
ld_child.wait()?;
|
||||
let codesign_child = Command::new("codesign")
|
||||
.args(["-s", "-", output_path.to_str().unwrap()])
|
||||
.spawn()?;
|
||||
|
||||
let mut codesign_cmd = Command::new("codesign");
|
||||
codesign_cmd.args(["-s", "-", output_path.to_str().unwrap()]);
|
||||
debug_print_command(&codesign_cmd);
|
||||
let codesign_child = codesign_cmd.spawn()?;
|
||||
|
||||
Ok((codesign_child, output_path))
|
||||
}
|
||||
@ -1179,8 +1186,11 @@ fn link_macos(
|
||||
}
|
||||
|
||||
fn get_macos_version() -> String {
|
||||
let cmd_stdout = Command::new("sw_vers")
|
||||
.arg("-productVersion")
|
||||
let mut cmd = Command::new("sw_vers");
|
||||
cmd.arg("-productVersion");
|
||||
debug_print_command(&cmd);
|
||||
|
||||
let cmd_stdout = cmd
|
||||
.output()
|
||||
.expect("Failed to execute command 'sw_vers -productVersion'")
|
||||
.stdout;
|
||||
@ -1383,15 +1393,11 @@ pub fn preprocess_host_wasm32(host_input_path: &Path, preprocessed_host_path: &P
|
||||
}
|
||||
|
||||
fn run_build_command(mut command: Command, file_to_build: &str, flaky_fail_counter: usize) {
|
||||
let mut command_string = std::ffi::OsString::new();
|
||||
command_string.push(command.get_program());
|
||||
|
||||
for arg in command.get_args() {
|
||||
command_string.push(" ");
|
||||
command_string.push(arg);
|
||||
}
|
||||
|
||||
let cmd_str = command_string.to_str().unwrap();
|
||||
let command_string = stringify_command(&command);
|
||||
let cmd_str = &command_string;
|
||||
roc_debug_flags::dbg_do!(roc_debug_flags::ROC_PRINT_BUILD_COMMANDS, {
|
||||
print_command_str(cmd_str);
|
||||
});
|
||||
let cmd_output = command.output().unwrap();
|
||||
let max_flaky_fail_count = 10;
|
||||
|
||||
@ -1429,3 +1435,41 @@ fn run_build_command(mut command: Command, file_to_build: &str, flaky_fail_count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stringify a command for printing
|
||||
/// e.g. `HOME=~ zig build-exe foo.zig -o foo`
|
||||
fn stringify_command(cmd: &Command) -> String {
|
||||
let mut command_string = std::ffi::OsString::new();
|
||||
|
||||
for (name, opt_val) in cmd.get_envs() {
|
||||
command_string.push(name);
|
||||
command_string.push("=");
|
||||
if let Some(val) = opt_val {
|
||||
command_string.push(val);
|
||||
} else {
|
||||
command_string.push("''");
|
||||
}
|
||||
command_string.push(" ");
|
||||
}
|
||||
|
||||
command_string.push(cmd.get_program());
|
||||
|
||||
for arg in cmd.get_args() {
|
||||
command_string.push(" ");
|
||||
command_string.push(arg);
|
||||
}
|
||||
|
||||
String::from(command_string.to_str().unwrap())
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn print_command_str(s: &str) {
|
||||
println!("\nRoc build command:\n{}\n", s);
|
||||
}
|
||||
|
||||
fn debug_print_command(_cmd: &Command) {
|
||||
// This debug macro is compiled out in release mode, so the argument is unused
|
||||
roc_debug_flags::dbg_do!(roc_debug_flags::ROC_PRINT_BUILD_COMMANDS, {
|
||||
print_command_str(&stringify_command(_cmd));
|
||||
});
|
||||
}
|
||||
|
@ -391,6 +391,29 @@ pub const RocDec = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn powInt(base: RocDec, exponent: i128) RocDec {
|
||||
if (exponent == 0) {
|
||||
return RocDec.one_point_zero;
|
||||
} else if (exponent > 0) {
|
||||
if (@mod(exponent, 2) == 0) {
|
||||
const half_power = RocDec.powInt(base, exponent >> 1); // `>> 1` == `/ 2`
|
||||
return RocDec.mul(half_power, half_power);
|
||||
} else {
|
||||
return RocDec.mul(base, RocDec.powInt(base, exponent - 1));
|
||||
}
|
||||
} else {
|
||||
return RocDec.div(RocDec.one_point_zero, RocDec.powInt(base, -exponent));
|
||||
}
|
||||
}
|
||||
|
||||
fn pow(base: RocDec, exponent: RocDec) RocDec {
|
||||
if (exponent.trunc().num == exponent.num) {
|
||||
return base.powInt(@divTrunc(exponent.num, RocDec.one_point_zero_i128));
|
||||
} else {
|
||||
return fromF64(std.math.pow(f64, base.toF64(), exponent.toF64())).?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mul(self: RocDec, other: RocDec) RocDec {
|
||||
const answer = RocDec.mulWithOverflow(self, other);
|
||||
|
||||
@ -1358,6 +1381,41 @@ test "round: -0.5" {
|
||||
try expectEqual(RocDec{ .num = -1000000000000000000 }, dec.round());
|
||||
}
|
||||
|
||||
test "powInt: 3.1 ^ 0" {
|
||||
var roc_str = RocStr.init("3.1", 3);
|
||||
var dec = RocDec.fromStr(roc_str).?;
|
||||
|
||||
try expectEqual(RocDec.one_point_zero, dec.powInt(0));
|
||||
}
|
||||
|
||||
test "powInt: 3.1 ^ 1" {
|
||||
var roc_str = RocStr.init("3.1", 3);
|
||||
var dec = RocDec.fromStr(roc_str).?;
|
||||
|
||||
try expectEqual(dec, dec.powInt(1));
|
||||
}
|
||||
|
||||
test "powInt: 2 ^ 2" {
|
||||
var roc_str = RocStr.init("4", 1);
|
||||
var dec = RocDec.fromStr(roc_str).?;
|
||||
|
||||
try expectEqual(dec, RocDec.two_point_zero.powInt(2));
|
||||
}
|
||||
|
||||
test "powInt: 0.5 ^ 2" {
|
||||
var roc_str = RocStr.init("0.25", 4);
|
||||
var dec = RocDec.fromStr(roc_str).?;
|
||||
|
||||
try expectEqual(dec, RocDec.zero_point_five.powInt(2));
|
||||
}
|
||||
|
||||
test "pow: 0.5 ^ 2.0" {
|
||||
var roc_str = RocStr.init("0.25", 4);
|
||||
var dec = RocDec.fromStr(roc_str).?;
|
||||
|
||||
try expectEqual(dec, RocDec.zero_point_five.pow(RocDec.two_point_zero));
|
||||
}
|
||||
|
||||
// exports
|
||||
|
||||
pub fn fromStr(arg: RocStr) callconv(.C) num_.NumParseResult(i128) {
|
||||
@ -1458,6 +1516,10 @@ pub fn logC(arg: RocDec) callconv(.C) i128 {
|
||||
return @call(.always_inline, RocDec.log, .{arg}).num;
|
||||
}
|
||||
|
||||
pub fn powC(arg1: RocDec, arg2: RocDec) callconv(.C) i128 {
|
||||
return @call(.always_inline, RocDec.pow, .{ arg1, arg2 }).num;
|
||||
}
|
||||
|
||||
pub fn sinC(arg: RocDec) callconv(.C) i128 {
|
||||
return @call(.always_inline, RocDec.sin, .{arg}).num;
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ comptime {
|
||||
exportDecFn(dec.fromStr, "from_str");
|
||||
exportDecFn(dec.fromU64C, "from_u64");
|
||||
exportDecFn(dec.logC, "log");
|
||||
exportDecFn(dec.powC, "pow");
|
||||
exportDecFn(dec.mulC, "mul_with_overflow");
|
||||
exportDecFn(dec.mulOrPanicC, "mul_or_panic");
|
||||
exportDecFn(dec.mulSaturatedC, "mul_saturated");
|
||||
|
@ -858,7 +858,7 @@ increaseSize = \@Dict { data, maxBucketCapacity, maxLoadFactor, shifts } ->
|
||||
shifts: newShifts,
|
||||
}
|
||||
else
|
||||
crash "Dict hit limit of \(Num.toStr maxBucketCount) elements. Unable to grow more."
|
||||
crash "Dict hit limit of $(Num.toStr maxBucketCount) elements. Unable to grow more."
|
||||
|
||||
allocBucketsFromShift : U8, F32 -> (List Bucket, U64)
|
||||
allocBucketsFromShift = \shifts, maxLoadFactor ->
|
||||
|
@ -824,7 +824,7 @@ replaceFirst : Str, Str, Str -> Str
|
||||
replaceFirst = \haystack, needle, flower ->
|
||||
when splitFirst haystack needle is
|
||||
Ok { before, after } ->
|
||||
"\(before)\(flower)\(after)"
|
||||
"$(before)$(flower)$(after)"
|
||||
|
||||
Err NotFound -> haystack
|
||||
|
||||
@ -842,7 +842,7 @@ replaceLast : Str, Str, Str -> Str
|
||||
replaceLast = \haystack, needle, flower ->
|
||||
when splitLast haystack needle is
|
||||
Ok { before, after } ->
|
||||
"\(before)\(flower)\(after)"
|
||||
"$(before)$(flower)$(after)"
|
||||
|
||||
Err NotFound -> haystack
|
||||
|
||||
|
@ -130,10 +130,10 @@ impl IntWidth {
|
||||
// according to https://reviews.llvm.org/D28990#655487
|
||||
//
|
||||
// however, rust does not always think that this is true
|
||||
// Our alignmets here are correct, but they will not match rust/zig/llvm until they update to llvm version 18.
|
||||
match target_info.architecture {
|
||||
Architecture::X86_64 => 16,
|
||||
Architecture::Aarch64 | Architecture::Aarch32 | Architecture::Wasm32 => 16,
|
||||
Architecture::X86_32 => 8,
|
||||
Architecture::X86_64 | Architecture::Aarch64 | Architecture::X86_32 => 16,
|
||||
Architecture::Aarch32 | Architecture::Wasm32 => 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -403,6 +403,7 @@ pub const DEC_FROM_INT: IntrinsicName = int_intrinsic!("roc_builtins.dec.from_in
|
||||
pub const DEC_FROM_STR: &str = "roc_builtins.dec.from_str";
|
||||
pub const DEC_FROM_U64: &str = "roc_builtins.dec.from_u64";
|
||||
pub const DEC_LOG: &str = "roc_builtins.dec.log";
|
||||
pub const DEC_POW: &str = "roc_builtins.dec.pow";
|
||||
pub const DEC_MUL_OR_PANIC: &str = "roc_builtins.dec.mul_or_panic";
|
||||
pub const DEC_MUL_SATURATED: &str = "roc_builtins.dec.mul_saturated";
|
||||
pub const DEC_MUL_WITH_OVERFLOW: &str = "roc_builtins.dec.mul_with_overflow";
|
||||
|
@ -126,7 +126,6 @@ map_symbol_to_lowlevel_and_arity! {
|
||||
StrSubstringUnsafe; STR_SUBSTRING_UNSAFE; 3,
|
||||
StrReserve; STR_RESERVE; 2,
|
||||
StrToNum; STR_TO_NUM; 1,
|
||||
StrGetCapacity; STR_CAPACITY; 1,
|
||||
StrWithCapacity; STR_WITH_CAPACITY; 1,
|
||||
StrReleaseExcessCapacity; STR_RELEASE_EXCESS_CAPACITY; 1,
|
||||
|
||||
|
@ -1051,14 +1051,13 @@ fn fix_values_captured_in_closure_expr(
|
||||
debug_assert!(!captures.is_empty());
|
||||
captured_symbols.extend(captures);
|
||||
captured_symbols.swap_remove(i);
|
||||
// Jump two, because the next element is now one of the newly-added captures,
|
||||
// which we don't need to check.
|
||||
i += 2;
|
||||
|
||||
added_captures = true;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Always jump one, because the current element either does not have captures or
|
||||
// is now one of the newly-added captures, which we don't need to check.
|
||||
i += 1;
|
||||
}
|
||||
if added_captures {
|
||||
// Re-sort, since we've added new captures.
|
||||
|
@ -13,7 +13,7 @@ use crate::{
|
||||
},
|
||||
pattern::{DestructType, Pattern, RecordDestruct, TupleDestruct},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum DeclarationInfo<'a> {
|
||||
Value {
|
||||
loc_symbol: Loc<Symbol>,
|
||||
@ -164,7 +164,7 @@ pub fn walk_decls<V: Visitor>(visitor: &mut V, decls: &Declarations) {
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_decl<V: Visitor>(visitor: &mut V, decl: DeclarationInfo<'_>) {
|
||||
pub fn walk_decl<V: Visitor>(visitor: &mut V, decl: DeclarationInfo<'_>) {
|
||||
use DeclarationInfo::*;
|
||||
|
||||
match decl {
|
||||
|
@ -1831,7 +1831,7 @@ mod test_can {
|
||||
// "abcd\$(efg)hij"
|
||||
// "#
|
||||
// ),
|
||||
// Str(r"abcd\(efg)hij".into()),
|
||||
// Str(r"abcd$(efg)hij".into()),
|
||||
// );
|
||||
// }
|
||||
|
||||
|
@ -173,4 +173,7 @@ flags! {
|
||||
|
||||
/// Don't build and use the subs cache (speeds up compilation of load and previous crates)
|
||||
ROC_SKIP_SUBS_CACHE
|
||||
|
||||
/// Print out shell commands used to buid the Roc and host code
|
||||
ROC_PRINT_BUILD_COMMANDS
|
||||
}
|
||||
|
@ -358,27 +358,21 @@ trait Backend<'a> {
|
||||
where
|
||||
I: Iterator<Item = InLayout<'b>>,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
use std::hash::{BuildHasher, Hash, Hasher};
|
||||
|
||||
let symbol = name.name();
|
||||
|
||||
let mut buf = String::with_capacity(1024);
|
||||
|
||||
// NOTE: due to randomness, this will not be consistent between runs
|
||||
let mut state = roc_collections::all::BuildHasher::default().build_hasher();
|
||||
for a in arguments {
|
||||
write!(buf, "{:?}", self.interner().dbg_stable(a)).expect("capacity");
|
||||
a.hash(&mut state);
|
||||
}
|
||||
|
||||
// lambda set should not matter; it should already be added as an argument
|
||||
// but the niche of the lambda name may be the only thing differentiating two different
|
||||
// implementations of a function with the same symbol
|
||||
write!(buf, "{:?}", name.niche().dbg_stable(self.interner())).expect("capacity");
|
||||
|
||||
write!(buf, "{:?}", self.interner().dbg_stable(result)).expect("capacity");
|
||||
|
||||
// NOTE: due to randomness, this will not be consistent between runs
|
||||
let mut state = roc_collections::all::BuildHasher::default().build_hasher();
|
||||
buf.hash(&mut state);
|
||||
name.niche().hash(&mut state);
|
||||
result.hash(&mut state);
|
||||
|
||||
let interns = self.interns();
|
||||
let ident_string = symbol.as_str(interns);
|
||||
@ -1099,7 +1093,7 @@ trait Backend<'a> {
|
||||
LayoutRepr::Builtin(Builtin::Float(float_width)) => {
|
||||
&bitcode::NUM_POW[float_width]
|
||||
}
|
||||
LayoutRepr::DEC => todo!("exponentiation for decimals"),
|
||||
LayoutRepr::DEC => bitcode::DEC_POW,
|
||||
_ => unreachable!("invalid layout for NumPow"),
|
||||
};
|
||||
|
||||
|
@ -1071,17 +1071,19 @@ pub fn module_from_builtins<'ctx>(
|
||||
// Anything not depended on by a `roc_builtin.` function could already by DCE'd theoretically.
|
||||
// That said, this workaround is good enough and fixes compilations times.
|
||||
|
||||
// Also, must_keep is the functions we depend on that would normally be provide by libc.
|
||||
// Also, must_keep is the functions we depend on that would normally be provide by libc or compiler-rt.
|
||||
// They are magically linked to by llvm builtins, so we must specify that they can't be DCE'd.
|
||||
let must_keep = [
|
||||
// Windows special required when floats are used
|
||||
"_fltused",
|
||||
// From libc
|
||||
"floorf",
|
||||
"memcpy",
|
||||
"memset",
|
||||
// I have no idea why this function is special.
|
||||
// Without it, some tests hang on M1 mac outside of nix.
|
||||
// From compiler-rt
|
||||
"__divti3",
|
||||
"__modti3",
|
||||
"__muloti4",
|
||||
// fixes `Undefined Symbol in relocation`
|
||||
"__udivti3",
|
||||
// Roc special functions
|
||||
"__roc_force_longjmp",
|
||||
@ -6751,9 +6753,7 @@ pub fn to_cc_return<'a>(
|
||||
) -> CCReturn {
|
||||
let return_size = layout_interner.stack_size(layout);
|
||||
let pass_result_by_pointer = match env.target_info.operating_system {
|
||||
roc_target::OperatingSystem::Windows => {
|
||||
return_size >= 2 * env.target_info.ptr_width() as u32
|
||||
}
|
||||
roc_target::OperatingSystem::Windows => return_size > env.target_info.ptr_width() as u32,
|
||||
roc_target::OperatingSystem::Unix => return_size > 2 * env.target_info.ptr_width() as u32,
|
||||
roc_target::OperatingSystem::Wasi => return_size > 2 * env.target_info.ptr_width() as u32,
|
||||
};
|
||||
|
@ -533,12 +533,6 @@ pub(crate) fn run_low_level<'a, 'ctx>(
|
||||
bitcode::STR_COUNT_UTF8_BYTES,
|
||||
)
|
||||
}
|
||||
StrGetCapacity => {
|
||||
// Str.capacity : Str -> U64
|
||||
arguments!(string);
|
||||
|
||||
call_bitcode_fn(env, &[string], bitcode::STR_CAPACITY)
|
||||
}
|
||||
StrSubstringUnsafe => {
|
||||
// Str.substringUnsafe : Str, U64, U64 -> Str
|
||||
arguments!(string, start, length);
|
||||
@ -2011,6 +2005,54 @@ fn dec_unary_op<'ctx>(
|
||||
}
|
||||
}
|
||||
|
||||
fn dec_binary_op<'ctx>(
|
||||
env: &Env<'_, 'ctx, '_>,
|
||||
fn_name: &str,
|
||||
dec1: BasicValueEnum<'ctx>,
|
||||
dec2: BasicValueEnum<'ctx>,
|
||||
) -> BasicValueEnum<'ctx> {
|
||||
use roc_target::Architecture::*;
|
||||
use roc_target::OperatingSystem::*;
|
||||
|
||||
let dec1 = dec1.into_int_value();
|
||||
let dec2 = dec2.into_int_value();
|
||||
|
||||
match env.target_info {
|
||||
TargetInfo {
|
||||
architecture: X86_64 | X86_32,
|
||||
operating_system: Unix,
|
||||
} => {
|
||||
let (low1, high1) = dec_split_into_words(env, dec1);
|
||||
let (low2, high2) = dec_split_into_words(env, dec2);
|
||||
let lowr_highr = call_bitcode_fn(
|
||||
env,
|
||||
&[low1.into(), high1.into(), low2.into(), high2.into()],
|
||||
fn_name,
|
||||
);
|
||||
|
||||
let block = env.builder.get_insert_block().expect("to be in a function");
|
||||
let parent = block.get_parent().expect("to be in a function");
|
||||
|
||||
let ptr =
|
||||
create_entry_block_alloca(env, parent, env.context.i128_type().into(), "to_i128");
|
||||
env.builder.build_store(ptr, lowr_highr).unwrap();
|
||||
|
||||
env.builder
|
||||
.build_load(env.context.i128_type(), ptr, "to_i128")
|
||||
.unwrap()
|
||||
}
|
||||
TargetInfo {
|
||||
architecture: Wasm32,
|
||||
operating_system: Unix,
|
||||
} => call_bitcode_fn(env, &[dec1.into(), dec2.into()], fn_name),
|
||||
_ => call_bitcode_fn(
|
||||
env,
|
||||
&[dec_alloca(env, dec1), dec_alloca(env, dec2)],
|
||||
fn_name,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn dec_binop_with_overflow<'ctx>(
|
||||
env: &Env<'_, 'ctx, '_>,
|
||||
fn_name: &str,
|
||||
@ -2229,6 +2271,7 @@ fn build_dec_binop<'a, 'ctx>(
|
||||
&[lhs, rhs],
|
||||
&bitcode::NUM_GREATER_THAN_OR_EQUAL[IntWidth::I128],
|
||||
),
|
||||
NumPow => dec_binary_op(env, bitcode::DEC_POW, lhs, rhs),
|
||||
_ => {
|
||||
unreachable!("Unrecognized dec binary operation: {:?}", op);
|
||||
}
|
||||
|
@ -204,7 +204,6 @@ impl<'a> LowLevelCall<'a> {
|
||||
StrCountUtf8Bytes => {
|
||||
self.load_args_and_call_zig(backend, bitcode::STR_COUNT_UTF8_BYTES)
|
||||
}
|
||||
StrGetCapacity => self.load_args_and_call_zig(backend, bitcode::STR_CAPACITY),
|
||||
StrToNum => {
|
||||
let number_layout = match backend.layout_interner.get_repr(self.ret_layout) {
|
||||
LayoutRepr::Struct(field_layouts) => field_layouts[0],
|
||||
@ -1589,6 +1588,10 @@ impl<'a> LowLevelCall<'a> {
|
||||
LayoutRepr::Builtin(Builtin::Float(width)) => {
|
||||
self.load_args_and_call_zig(backend, &bitcode::NUM_POW[width]);
|
||||
}
|
||||
LayoutRepr::Builtin(Builtin::Decimal) => {
|
||||
self.load_args_and_call_zig(backend, bitcode::DEC_POW);
|
||||
}
|
||||
|
||||
_ => panic_ret_type(),
|
||||
},
|
||||
|
||||
|
@ -22,8 +22,21 @@ macro_rules! wasm32_sized_primitive {
|
||||
}
|
||||
}
|
||||
|
||||
wasm32_sized_primitive!(u8, i8, u16, i16, u32, i32, char, u64, i64, u128, i128, f32, f64, bool,);
|
||||
wasm32_sized_primitive!(RocDec, RocOrder, I128, U128,);
|
||||
wasm32_sized_primitive!(u8, i8, u16, i16, u32, i32, char, u64, i64, f32, f64, bool,);
|
||||
wasm32_sized_primitive!(RocOrder,);
|
||||
|
||||
macro_rules! wasm32_16byte_aligned8 {
|
||||
($($type_name:ident ,)+) => {
|
||||
$(
|
||||
impl Wasm32Sized for $type_name {
|
||||
const SIZE_OF_WASM: usize = 16;
|
||||
const ALIGN_OF_WASM: usize = 8;
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
wasm32_16byte_aligned8!(i128, u128, I128, U128, RocDec,);
|
||||
|
||||
impl Wasm32Sized for () {
|
||||
const SIZE_OF_WASM: usize = 0;
|
||||
|
@ -5549,7 +5549,7 @@ mod test_reporting {
|
||||
r#"
|
||||
greeting = "Privet"
|
||||
|
||||
if Bool.true then 1 else "\(greeting), World!"
|
||||
if Bool.true then 1 else "$(greeting), World!"
|
||||
"#,
|
||||
),
|
||||
@r#"
|
||||
@ -5557,7 +5557,7 @@ mod test_reporting {
|
||||
|
||||
This `if` has an `else` branch with a different type from its `then` branch:
|
||||
|
||||
6│ if Bool.true then 1 else "\(greeting), World!"
|
||||
6│ if Bool.true then 1 else "$(greeting), World!"
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The `else` branch is a string of type:
|
||||
|
@ -3838,6 +3838,35 @@ struct HeaderOutput<'a> {
|
||||
opt_platform_shorthand: Option<&'a str>,
|
||||
}
|
||||
|
||||
fn ensure_roc_file<'a>(filename: &Path, src_bytes: &[u8]) -> Result<(), LoadingProblem<'a>> {
|
||||
match filename.extension() {
|
||||
Some(ext) => {
|
||||
if ext != ROC_FILE_EXTENSION {
|
||||
return Err(LoadingProblem::FileProblem {
|
||||
filename: filename.to_path_buf(),
|
||||
error: io::ErrorKind::Unsupported,
|
||||
});
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let index = src_bytes
|
||||
.iter()
|
||||
.position(|a| *a == b'\n')
|
||||
.unwrap_or(src_bytes.len());
|
||||
let frist_line_bytes = src_bytes[0..index].to_vec();
|
||||
if let Ok(first_line) = String::from_utf8(frist_line_bytes) {
|
||||
if !(first_line.starts_with("#!") && first_line.contains("roc")) {
|
||||
return Err(LoadingProblem::FileProblem {
|
||||
filename: filename.to_path_buf(),
|
||||
error: std::io::ErrorKind::Unsupported,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_header<'a>(
|
||||
arena: &'a Bump,
|
||||
read_file_duration: Duration,
|
||||
@ -3856,6 +3885,8 @@ fn parse_header<'a>(
|
||||
let parsed = roc_parse::module::parse_header(arena, parse_state.clone());
|
||||
let parse_header_duration = parse_start.elapsed();
|
||||
|
||||
ensure_roc_file(&filename, src_bytes)?;
|
||||
|
||||
// Insert the first entries for this module's timings
|
||||
let mut module_timing = ModuleTiming::new(start_time);
|
||||
|
||||
|
@ -323,7 +323,7 @@ fn import_transitive_alias() {
|
||||
// with variables in the importee
|
||||
let modules = vec![
|
||||
(
|
||||
"RBTree",
|
||||
"RBTree.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface RBTree exposes [RedBlackTree, empty] imports []
|
||||
@ -341,7 +341,7 @@ fn import_transitive_alias() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"Other",
|
||||
"Other.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface Other exposes [empty] imports [RBTree]
|
||||
@ -626,7 +626,7 @@ fn ingested_file_bytes() {
|
||||
#[test]
|
||||
fn parse_problem() {
|
||||
let modules = vec![(
|
||||
"Main",
|
||||
"Main.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface Main exposes [main] imports []
|
||||
@ -641,7 +641,7 @@ fn parse_problem() {
|
||||
report,
|
||||
indoc!(
|
||||
"
|
||||
── UNFINISHED LIST in tmp/parse_problem/Main ───────────────────────────────────
|
||||
── UNFINISHED LIST in tmp/parse_problem/Main.roc ───────────────────────────────
|
||||
|
||||
I am partway through started parsing a list, but I got stuck here:
|
||||
|
||||
@ -707,7 +707,7 @@ fn ingested_file_not_found() {
|
||||
#[test]
|
||||
fn platform_does_not_exist() {
|
||||
let modules = vec![(
|
||||
"Main",
|
||||
"main.roc",
|
||||
indoc!(
|
||||
r#"
|
||||
app "example"
|
||||
@ -753,7 +753,7 @@ fn platform_parse_error() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"Main",
|
||||
"main.roc",
|
||||
indoc!(
|
||||
r#"
|
||||
app "hello-world"
|
||||
@ -797,7 +797,7 @@ fn platform_exposes_main_return_by_pointer_issue() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"Main",
|
||||
"main.roc",
|
||||
indoc!(
|
||||
r#"
|
||||
app "hello-world"
|
||||
@ -818,7 +818,7 @@ fn platform_exposes_main_return_by_pointer_issue() {
|
||||
fn opaque_wrapped_unwrapped_outside_defining_module() {
|
||||
let modules = vec![
|
||||
(
|
||||
"Age",
|
||||
"Age.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface Age exposes [Age] imports []
|
||||
@ -828,7 +828,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"Main",
|
||||
"Main.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface Main exposes [twenty, readAge] imports [Age.{ Age }]
|
||||
@ -847,7 +847,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
|
||||
err,
|
||||
indoc!(
|
||||
r"
|
||||
── OPAQUE TYPE DECLARED OUTSIDE SCOPE in ...apped_outside_defining_module/Main ─
|
||||
── OPAQUE TYPE DECLARED OUTSIDE SCOPE in ...d_outside_defining_module/Main.roc ─
|
||||
|
||||
The unwrapped opaque type Age referenced here:
|
||||
|
||||
@ -861,7 +861,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
|
||||
|
||||
Note: Opaque types can only be wrapped and unwrapped in the module they are defined in!
|
||||
|
||||
── OPAQUE TYPE DECLARED OUTSIDE SCOPE in ...apped_outside_defining_module/Main ─
|
||||
── OPAQUE TYPE DECLARED OUTSIDE SCOPE in ...d_outside_defining_module/Main.roc ─
|
||||
|
||||
The unwrapped opaque type Age referenced here:
|
||||
|
||||
@ -875,7 +875,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
|
||||
|
||||
Note: Opaque types can only be wrapped and unwrapped in the module they are defined in!
|
||||
|
||||
── UNUSED IMPORT in tmp/opaque_wrapped_unwrapped_outside_defining_module/Main ──
|
||||
── UNUSED IMPORT in ...aque_wrapped_unwrapped_outside_defining_module/Main.roc ─
|
||||
|
||||
Nothing from Age is used in this module.
|
||||
|
||||
@ -910,7 +910,7 @@ fn issue_2863_module_type_does_not_exist() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"Main",
|
||||
"main.roc",
|
||||
indoc!(
|
||||
r#"
|
||||
app "test"
|
||||
@ -930,7 +930,7 @@ fn issue_2863_module_type_does_not_exist() {
|
||||
report,
|
||||
indoc!(
|
||||
"
|
||||
── UNRECOGNIZED NAME in tmp/issue_2863_module_type_does_not_exist/Main ─────────
|
||||
── UNRECOGNIZED NAME in tmp/issue_2863_module_type_does_not_exist/main.roc ─────
|
||||
|
||||
Nothing is named `DoesNotExist` in this scope.
|
||||
|
||||
@ -971,7 +971,7 @@ fn import_builtin_in_platform_and_check_app() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"Main",
|
||||
"main.roc",
|
||||
indoc!(
|
||||
r#"
|
||||
app "test"
|
||||
@ -991,7 +991,7 @@ fn import_builtin_in_platform_and_check_app() {
|
||||
#[test]
|
||||
fn module_doesnt_match_file_path() {
|
||||
let modules = vec![(
|
||||
"Age",
|
||||
"Age.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface NotAge exposes [Age] imports []
|
||||
@ -1006,7 +1006,7 @@ fn module_doesnt_match_file_path() {
|
||||
err,
|
||||
indoc!(
|
||||
r"
|
||||
── WEIRD MODULE NAME in tmp/module_doesnt_match_file_path/Age ──────────────────
|
||||
── WEIRD MODULE NAME in tmp/module_doesnt_match_file_path/Age.roc ──────────────
|
||||
|
||||
This module name does not correspond with the file path it is defined
|
||||
in:
|
||||
@ -1026,7 +1026,7 @@ fn module_doesnt_match_file_path() {
|
||||
#[test]
|
||||
fn module_cyclic_import_itself() {
|
||||
let modules = vec![(
|
||||
"Age",
|
||||
"Age.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface Age exposes [] imports [Age]
|
||||
@ -1039,7 +1039,7 @@ fn module_cyclic_import_itself() {
|
||||
err,
|
||||
indoc!(
|
||||
r"
|
||||
── IMPORT CYCLE in tmp/module_cyclic_import_itself/Age ─────────────────────────
|
||||
── IMPORT CYCLE in tmp/module_cyclic_import_itself/Age.roc ─────────────────────
|
||||
|
||||
I can't compile Age because it depends on itself through the following
|
||||
chain of module imports:
|
||||
@ -1062,7 +1062,7 @@ fn module_cyclic_import_itself() {
|
||||
fn module_cyclic_import_transitive() {
|
||||
let modules = vec![
|
||||
(
|
||||
"Age",
|
||||
"Age.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface Age exposes [] imports [Person]
|
||||
@ -1070,7 +1070,7 @@ fn module_cyclic_import_transitive() {
|
||||
),
|
||||
),
|
||||
(
|
||||
"Person",
|
||||
"Person.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface Person exposes [] imports [Age]
|
||||
@ -1150,7 +1150,7 @@ fn nested_module_has_incorrect_name() {
|
||||
#[test]
|
||||
fn module_interface_with_qualified_import() {
|
||||
let modules = vec![(
|
||||
"A",
|
||||
"A.roc",
|
||||
indoc!(
|
||||
r"
|
||||
interface A exposes [] imports [b.T]
|
||||
@ -1163,7 +1163,7 @@ fn module_interface_with_qualified_import() {
|
||||
err,
|
||||
indoc!(
|
||||
r#"
|
||||
The package shorthand 'b' that you are using in the 'imports' section of the header of module 'tmp/module_interface_with_qualified_import/A' doesn't exist.
|
||||
The package shorthand 'b' that you are using in the 'imports' section of the header of module 'tmp/module_interface_with_qualified_import/A.roc' doesn't exist.
|
||||
Check that package shorthand is correct or reference the package in an 'app' or 'package' header.
|
||||
This module is an interface, because of a bug in the compiler we are unable to directly typecheck interface modules with package imports so this error may not be correct. Please start checking at an app, package or platform file that imports this file."#
|
||||
),
|
||||
@ -1174,7 +1174,7 @@ fn module_interface_with_qualified_import() {
|
||||
#[test]
|
||||
fn app_missing_package_import() {
|
||||
let modules = vec![(
|
||||
"Main",
|
||||
"main.roc",
|
||||
indoc!(
|
||||
r#"
|
||||
app "example"
|
||||
@ -1192,10 +1192,73 @@ fn app_missing_package_import() {
|
||||
err,
|
||||
indoc!(
|
||||
r#"
|
||||
The package shorthand 'notpack' that you are using in the 'imports' section of the header of module 'tmp/app_missing_package_import/Main' doesn't exist.
|
||||
The package shorthand 'notpack' that you are using in the 'imports' section of the header of module 'tmp/app_missing_package_import/main.roc' doesn't exist.
|
||||
Check that package shorthand is correct or reference the package in an 'app' or 'package' header."#
|
||||
),
|
||||
"\n{}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_roc_file_extension() {
|
||||
let modules = vec![(
|
||||
"main.md",
|
||||
indoc!(
|
||||
r"
|
||||
# Not a roc file
|
||||
"
|
||||
),
|
||||
)];
|
||||
|
||||
let expected = indoc!(
|
||||
r"
|
||||
── NOT A ROC FILE in tmp/non_roc_file_extension/main.md ────────────────────────
|
||||
|
||||
I expected a file with extension `.roc` or without extension.
|
||||
Instead I received a file with extension `.md`."
|
||||
);
|
||||
let color_start = String::from_utf8(vec![27, 91, 51, 54, 109]).unwrap();
|
||||
let color_end = String::from_utf8(vec![27, 91, 48, 109]).unwrap();
|
||||
let err = multiple_modules("non_roc_file_extension", modules).unwrap_err();
|
||||
let err = err.replace(&color_start, "");
|
||||
let err = err.replace(&color_end, "");
|
||||
assert_eq!(err, expected, "\n{}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roc_file_no_extension() {
|
||||
let modules = vec![(
|
||||
"main",
|
||||
indoc!(
|
||||
r#"
|
||||
app "helloWorld"
|
||||
packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.8.1/x8URkvfyi9I0QhmVG98roKBUs_AZRkLFwFJVJ3942YA.tar.br" }
|
||||
imports [pf.Stdout]
|
||||
provides [main] to pf
|
||||
|
||||
main =
|
||||
Stdout.line "Hello, World!"
|
||||
"#
|
||||
),
|
||||
)];
|
||||
|
||||
let expected = indoc!(
|
||||
r"
|
||||
── NOT A ROC FILE in tmp/roc_file_no_extension/main ────────────────────────────
|
||||
|
||||
I expected a file with either:
|
||||
- extension `.roc`
|
||||
- no extension and a roc shebang as the first line, e.g.
|
||||
`#!/home/username/bin/roc_nightly/roc`
|
||||
|
||||
The provided file did not start with a shebang `#!` containing the
|
||||
string `roc`. Is tmp/roc_file_no_extension/main a Roc file?"
|
||||
);
|
||||
let color_start = String::from_utf8(vec![27, 91, 51, 54, 109]).unwrap();
|
||||
let color_end = String::from_utf8(vec![27, 91, 48, 109]).unwrap();
|
||||
let err = multiple_modules("roc_file_no_extension", modules).unwrap_err();
|
||||
let err = err.replace(&color_start, "");
|
||||
let err = err.replace(&color_end, "");
|
||||
assert_eq!(err, expected, "\n{}", err);
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ pub enum CalledVia {
|
||||
UnaryOp(UnaryOp),
|
||||
|
||||
/// This call is the result of desugaring string interpolation,
|
||||
/// e.g. "\(first) \(last)" is transformed into Str.concat (Str.concat first " ") last.
|
||||
/// e.g. "$(first) $(last)" is transformed into Str.concat (Str.concat first " ") last.
|
||||
StringInterpolation,
|
||||
|
||||
/// This call is the result of desugaring a Record Builder field.
|
||||
|
@ -24,7 +24,6 @@ pub enum LowLevel {
|
||||
StrGetUnsafe,
|
||||
StrSubstringUnsafe,
|
||||
StrReserve,
|
||||
StrGetCapacity,
|
||||
StrWithCapacity,
|
||||
StrReleaseExcessCapacity,
|
||||
ListLen,
|
||||
@ -267,7 +266,6 @@ map_symbol_to_lowlevel! {
|
||||
StrSubstringUnsafe <= STR_SUBSTRING_UNSAFE;
|
||||
StrReserve <= STR_RESERVE;
|
||||
StrToNum <= STR_TO_NUM;
|
||||
StrGetCapacity <= STR_CAPACITY;
|
||||
StrWithCapacity <= STR_WITH_CAPACITY;
|
||||
StrReleaseExcessCapacity <= STR_RELEASE_EXCESS_CAPACITY;
|
||||
ListLen <= LIST_LEN;
|
||||
|
@ -1533,8 +1533,8 @@ fn low_level_no_rc(lowlevel: &LowLevel) -> RC {
|
||||
|
||||
match lowlevel {
|
||||
Unreachable => RC::Uknown,
|
||||
ListLen | StrIsEmpty | StrCountUtf8Bytes | StrGetCapacity | ListGetCapacity
|
||||
| ListWithCapacity | StrWithCapacity => RC::NoRc,
|
||||
ListLen | StrIsEmpty | StrCountUtf8Bytes | ListGetCapacity | ListWithCapacity
|
||||
| StrWithCapacity => RC::NoRc,
|
||||
ListReplaceUnsafe => RC::Rc,
|
||||
StrGetUnsafe | ListGetUnsafe => RC::NoRc,
|
||||
ListConcat => RC::Rc,
|
||||
|
@ -1283,7 +1283,7 @@ fn lowlevel_borrow_signature(arena: &Bump, op: LowLevel) -> &[Ownership] {
|
||||
match op {
|
||||
Unreachable => arena.alloc_slice_copy(&[irrelevant]),
|
||||
DictPseudoSeed => arena.alloc_slice_copy(&[irrelevant]),
|
||||
ListLen | StrIsEmpty | StrCountUtf8Bytes | StrGetCapacity | ListGetCapacity => {
|
||||
ListLen | StrIsEmpty | StrCountUtf8Bytes | ListGetCapacity => {
|
||||
arena.alloc_slice_copy(&[borrowed])
|
||||
}
|
||||
ListWithCapacity | StrWithCapacity => arena.alloc_slice_copy(&[irrelevant]),
|
||||
|
@ -5135,6 +5135,21 @@ pub fn with_hole<'a>(
|
||||
Err(_) => return runtime_error(env, "Can't update record with improper layout"),
|
||||
};
|
||||
|
||||
let sorted_fields_filtered =
|
||||
sorted_fields
|
||||
.iter()
|
||||
.filter_map(|(label, _, opt_field_layout)| {
|
||||
match opt_field_layout {
|
||||
Ok(_) => Some(label),
|
||||
Err(_) => {
|
||||
debug_assert!(!updates.contains_key(label));
|
||||
// this was an optional field, and now does not exist!
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
let sorted_fields = Vec::from_iter_in(sorted_fields_filtered, env.arena);
|
||||
|
||||
let single_field_struct = sorted_fields.len() == 1;
|
||||
|
||||
// The struct indexing generated by the current context
|
||||
@ -5143,44 +5158,38 @@ pub fn with_hole<'a>(
|
||||
let mut new_struct_symbols = Vec::with_capacity_in(sorted_fields.len(), env.arena);
|
||||
// Information about the fields that are being updated
|
||||
let mut fields = Vec::with_capacity_in(sorted_fields.len(), env.arena);
|
||||
let mut index = 0;
|
||||
for (label, _, opt_field_layout) in sorted_fields.iter() {
|
||||
let record_index = (structure, index);
|
||||
|
||||
match opt_field_layout {
|
||||
Err(_) => {
|
||||
debug_assert!(!updates.contains_key(label));
|
||||
// this was an optional field, and now does not exist!
|
||||
// do not increment `index`!
|
||||
}
|
||||
Ok(_field_layout) => {
|
||||
current_struct_indexing.push(record_index);
|
||||
// Create a symbol for each of the fields as they might be referenced later.
|
||||
// The struct with a single field is optimized in such a way that replacing later indexing will cause an incorrect IR.
|
||||
// Thus, only insert these struct_indices if there is more than one field in the struct.
|
||||
if !single_field_struct {
|
||||
for index in 0..sorted_fields.len() {
|
||||
let record_index = (structure, index as u64);
|
||||
|
||||
// The struct with a single field is optimized in such a way that replacing later indexing will cause an incorrect IR.
|
||||
// Thus, only insert these struct_indices if there is more than one field in the struct.
|
||||
if !single_field_struct {
|
||||
let original_struct_symbol = env.unique_symbol();
|
||||
env.struct_indexing
|
||||
.insert(record_index, original_struct_symbol);
|
||||
}
|
||||
if let Some(field) = updates.get(label) {
|
||||
let new_struct_symbol = possible_reuse_symbol_or_specialize(
|
||||
env,
|
||||
procs,
|
||||
layout_cache,
|
||||
&field.loc_expr.value,
|
||||
field.var,
|
||||
);
|
||||
new_struct_symbols.push(new_struct_symbol);
|
||||
fields.push(UpdateExisting(field));
|
||||
} else {
|
||||
new_struct_symbols
|
||||
.push(*env.struct_indexing.get(record_index).unwrap());
|
||||
fields.push(CopyExisting);
|
||||
}
|
||||
current_struct_indexing.push(record_index);
|
||||
|
||||
index += 1;
|
||||
}
|
||||
let original_struct_symbol = env.unique_symbol();
|
||||
env.struct_indexing
|
||||
.insert(record_index, original_struct_symbol);
|
||||
}
|
||||
}
|
||||
|
||||
for (index, label) in sorted_fields.iter().enumerate() {
|
||||
let record_index = (structure, index as u64);
|
||||
|
||||
if let Some(field) = updates.get(label) {
|
||||
let new_struct_symbol = possible_reuse_symbol_or_specialize(
|
||||
env,
|
||||
procs,
|
||||
layout_cache,
|
||||
&field.loc_expr.value,
|
||||
field.var,
|
||||
);
|
||||
new_struct_symbols.push(new_struct_symbol);
|
||||
fields.push(UpdateExisting(field));
|
||||
} else {
|
||||
new_struct_symbols.push(*env.struct_indexing.get(record_index).unwrap());
|
||||
fields.push(CopyExisting);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -364,7 +364,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LineInfo {
|
||||
line_offsets: Vec<u32>,
|
||||
}
|
||||
|
@ -312,7 +312,7 @@ mod solve_expr {
|
||||
r#"
|
||||
whatItIs = "great"
|
||||
|
||||
"type inference is \(whatItIs)!"
|
||||
"type inference is $(whatItIs)!"
|
||||
"#
|
||||
),
|
||||
"Str",
|
||||
@ -326,7 +326,7 @@ mod solve_expr {
|
||||
r#"
|
||||
whatItIs = "great"
|
||||
|
||||
str = "type inference is \(whatItIs)!"
|
||||
str = "type inference is $(whatItIs)!"
|
||||
|
||||
whatItIs
|
||||
"#
|
||||
@ -342,7 +342,7 @@ mod solve_expr {
|
||||
r#"
|
||||
rec = { whatItIs: "great" }
|
||||
|
||||
str = "type inference is \(rec.whatItIs)!"
|
||||
str = "type inference is $(rec.whatItIs)!"
|
||||
|
||||
rec
|
||||
"#
|
||||
@ -4739,7 +4739,7 @@ mod solve_expr {
|
||||
r#"
|
||||
setRocEmail : _ -> { name: Str, email: Str }_
|
||||
setRocEmail = \person ->
|
||||
{ person & email: "\(person.name)@roclang.com" }
|
||||
{ person & email: "$(person.name)@roclang.com" }
|
||||
setRocEmail
|
||||
"#
|
||||
),
|
||||
|
@ -948,7 +948,7 @@ fn specialize_unique_newtype_records() {
|
||||
main =
|
||||
when Str.fromUtf8 (Encode.toBytes {a: Bool.true} TotallyNotJson.json) is
|
||||
Ok s -> when Str.fromUtf8 (Encode.toBytes {b: Bool.true} TotallyNotJson.json) is
|
||||
Ok t -> "\(s)\(t)"
|
||||
Ok t -> "$(s)$(t)"
|
||||
_ -> "<bad>"
|
||||
_ -> "<bad>"
|
||||
"#
|
||||
|
@ -330,7 +330,7 @@ fn list_map_try_ok() {
|
||||
List.mapTry [1, 2, 3] \num ->
|
||||
str = Num.toStr (num * 2)
|
||||
|
||||
Ok "\(str)!"
|
||||
Ok "$(str)!"
|
||||
"#,
|
||||
// Result Str [] is unwrapped to just Str
|
||||
RocList::<RocStr>::from_slice(&[
|
||||
@ -3738,10 +3738,10 @@ fn issue_3571_lowlevel_call_function_with_bool_lambda_set() {
|
||||
List.concat state mappedVals
|
||||
|
||||
add2 : Str -> Str
|
||||
add2 = \x -> "added \(x)"
|
||||
add2 = \x -> "added $(x)"
|
||||
|
||||
mul2 : Str -> Str
|
||||
mul2 = \x -> "multiplied \(x)"
|
||||
mul2 = \x -> "multiplied $(x)"
|
||||
|
||||
foo = [add2, mul2]
|
||||
bar = ["1", "2", "3", "4"]
|
||||
|
@ -1825,10 +1825,16 @@ fn float_compare() {
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
|
||||
fn pow() {
|
||||
fn pow_f64() {
|
||||
assert_evals_to!("Num.pow 2.0f64 2.0f64", 4.0, f64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
|
||||
fn pow_dec() {
|
||||
assert_evals_to!("Num.pow 2.0dec 2.0dec", RocDec::from(4), RocDec);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
|
||||
fn round_f64() {
|
||||
|
@ -3199,7 +3199,7 @@ fn recursively_build_effect() {
|
||||
hi = "Hello"
|
||||
name = "World"
|
||||
|
||||
"\(hi), \(name)!"
|
||||
"$(hi), $(name)!"
|
||||
|
||||
main =
|
||||
when nestHelp 4 is
|
||||
@ -3955,8 +3955,8 @@ fn compose_recursive_lambda_set_productive_toplevel() {
|
||||
compose = \f, g -> \x -> g (f x)
|
||||
|
||||
identity = \x -> x
|
||||
exclaim = \s -> "\(s)!"
|
||||
whisper = \s -> "(\(s))"
|
||||
exclaim = \s -> "$(s)!"
|
||||
whisper = \s -> "($(s))"
|
||||
|
||||
main =
|
||||
res: Str -> Str
|
||||
@ -3978,8 +3978,8 @@ fn compose_recursive_lambda_set_productive_nested() {
|
||||
compose = \f, g -> \x -> g (f x)
|
||||
|
||||
identity = \x -> x
|
||||
exclaim = \s -> "\(s)!"
|
||||
whisper = \s -> "(\(s))"
|
||||
exclaim = \s -> "$(s)!"
|
||||
whisper = \s -> "($(s))"
|
||||
|
||||
res: Str -> Str
|
||||
res = List.walk [ exclaim, whisper ] identity compose
|
||||
@ -4000,8 +4000,8 @@ fn compose_recursive_lambda_set_productive_inferred() {
|
||||
compose = \f, g -> \x -> g (f x)
|
||||
|
||||
identity = \x -> x
|
||||
exclaim = \s -> "\(s)!"
|
||||
whisper = \s -> "(\(s))"
|
||||
exclaim = \s -> "$(s)!"
|
||||
whisper = \s -> "($(s))"
|
||||
|
||||
res = List.walk [ exclaim, whisper ] identity compose
|
||||
res "hello"
|
||||
@ -4026,8 +4026,8 @@ fn compose_recursive_lambda_set_productive_nullable_wrapped() {
|
||||
else \x -> f (g x)
|
||||
|
||||
identity = \x -> x
|
||||
exclame = \s -> "\(s)!"
|
||||
whisper = \s -> "(\(s))"
|
||||
exclame = \s -> "$(s)!"
|
||||
whisper = \s -> "($(s))"
|
||||
|
||||
main =
|
||||
res: Str -> Str
|
||||
@ -4554,7 +4554,7 @@ fn reset_recursive_type_wraps_in_named_type() {
|
||||
Cons x xs ->
|
||||
strX = f x
|
||||
strXs = printLinkedList xs f
|
||||
"Cons \(strX) (\(strXs))"
|
||||
"Cons $(strX) ($(strXs))"
|
||||
"#
|
||||
),
|
||||
RocStr::from("Cons 2 (Cons 3 (Cons 4 (Nil)))"),
|
||||
|
11
crates/compiler/test_mono/generated/issue_5513.txt
Normal file
11
crates/compiler/test_mono/generated/issue_5513.txt
Normal file
@ -0,0 +1,11 @@
|
||||
procedure Test.1 (Test.2):
|
||||
let Test.7 : I64 = StructAtIndex 1 Test.2;
|
||||
let Test.5 : {I64, I64} = Struct {Test.7, Test.7};
|
||||
ret Test.5;
|
||||
|
||||
procedure Test.0 ():
|
||||
let Test.8 : I64 = 0i64;
|
||||
let Test.9 : I64 = 0i64;
|
||||
let Test.4 : {I64, I64} = Struct {Test.8, Test.9};
|
||||
let Test.3 : {I64, I64} = CallByName Test.1 Test.4;
|
||||
ret Test.3;
|
37
crates/compiler/test_mono/generated/issue_6174.txt
Normal file
37
crates/compiler/test_mono/generated/issue_6174.txt
Normal file
@ -0,0 +1,37 @@
|
||||
procedure Bool.1 ():
|
||||
let Bool.23 : Int1 = false;
|
||||
ret Bool.23;
|
||||
|
||||
procedure Test.2 (Test.11, Test.1):
|
||||
if Test.1 then
|
||||
let Test.29 : I64 = 0i64;
|
||||
let Test.28 : [C {}, C I64] = TagId(1) Test.29;
|
||||
ret Test.28;
|
||||
else
|
||||
let Test.27 : {} = Struct {};
|
||||
let Test.26 : [C {}, C I64] = TagId(0) Test.27;
|
||||
ret Test.26;
|
||||
|
||||
procedure Test.3 (Test.12, Test.1):
|
||||
if Test.1 then
|
||||
let Test.23 : I64 = 0i64;
|
||||
let Test.22 : [C {}, C I64] = TagId(1) Test.23;
|
||||
ret Test.22;
|
||||
else
|
||||
let Test.21 : {} = Struct {};
|
||||
let Test.20 : [C {}, C I64] = TagId(0) Test.21;
|
||||
ret Test.20;
|
||||
|
||||
procedure Test.4 (Test.13, Test.1):
|
||||
let Test.25 : {} = Struct {};
|
||||
let Test.17 : [C {}, C I64] = CallByName Test.2 Test.25 Test.1;
|
||||
let Test.19 : {} = Struct {};
|
||||
let Test.18 : [C {}, C I64] = CallByName Test.3 Test.19 Test.1;
|
||||
let Test.16 : List [C {}, C I64] = Array [Test.17, Test.18];
|
||||
ret Test.16;
|
||||
|
||||
procedure Test.0 ():
|
||||
let Test.1 : Int1 = CallByName Bool.1;
|
||||
let Test.15 : {} = Struct {};
|
||||
let Test.14 : List [C {}, C I64] = CallByName Test.4 Test.15 Test.1;
|
||||
ret Test.14;
|
@ -27,15 +27,15 @@ procedure Num.22 (#Attr.2, #Attr.3):
|
||||
|
||||
procedure Test.1 (Test.2):
|
||||
let Test.6 : List U64 = StructAtIndex 0 Test.2;
|
||||
let Test.8 : List U64 = StructAtIndex 1 Test.2;
|
||||
let Test.10 : List U64 = StructAtIndex 2 Test.2;
|
||||
let Test.7 : List U64 = StructAtIndex 1 Test.2;
|
||||
let Test.8 : List U64 = StructAtIndex 2 Test.2;
|
||||
let Test.13 : U64 = 8i64;
|
||||
let Test.14 : U64 = 8i64;
|
||||
let Test.9 : List U64 = CallByName List.3 Test.8 Test.13 Test.14;
|
||||
let Test.10 : List U64 = CallByName List.3 Test.7 Test.13 Test.14;
|
||||
let Test.11 : U64 = 7i64;
|
||||
let Test.12 : U64 = 7i64;
|
||||
let Test.7 : List U64 = CallByName List.3 Test.6 Test.11 Test.12;
|
||||
let Test.5 : {List U64, List U64, List U64} = Struct {Test.7, Test.9, Test.10};
|
||||
let Test.9 : List U64 = CallByName List.3 Test.6 Test.11 Test.12;
|
||||
let Test.5 : {List U64, List U64, List U64} = Struct {Test.9, Test.10, Test.8};
|
||||
ret Test.5;
|
||||
|
||||
procedure Test.0 ():
|
||||
|
@ -1679,7 +1679,7 @@ fn lambda_capture_niches_with_other_lambda_capture() {
|
||||
when val is
|
||||
_ -> ""
|
||||
|
||||
capture2 = \val -> \{} -> "\(val)"
|
||||
capture2 = \val -> \{} -> "$(val)"
|
||||
|
||||
x : [A, B, C]
|
||||
x = A
|
||||
@ -1984,7 +1984,7 @@ fn polymorphic_expression_unification() {
|
||||
]
|
||||
parseFunction : Str -> RenderTree
|
||||
parseFunction = \name ->
|
||||
last = Indent [Text ".trace(\"\(name)\")" ]
|
||||
last = Indent [Text ".trace(\"$(name)\")" ]
|
||||
Indent [last]
|
||||
|
||||
values = parseFunction "interface_header"
|
||||
@ -2540,7 +2540,7 @@ fn recursively_build_effect() {
|
||||
hi = "Hello"
|
||||
name = "World"
|
||||
|
||||
"\(hi), \(name)!"
|
||||
"$(hi), $(name)!"
|
||||
|
||||
main =
|
||||
when nestHelp 4 is
|
||||
@ -2856,8 +2856,8 @@ fn compose_recursive_lambda_set_productive_nullable_wrapped() {
|
||||
else \x -> f (g x)
|
||||
|
||||
identity = \x -> x
|
||||
exclame = \s -> "\(s)!"
|
||||
whisper = \s -> "(\(s))"
|
||||
exclame = \s -> "$(s)!"
|
||||
whisper = \s -> "($(s))"
|
||||
|
||||
main =
|
||||
res: Str -> Str
|
||||
@ -3463,3 +3463,40 @@ fn issue_6196() {
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
#[mono_test]
|
||||
fn issue_5513() {
|
||||
indoc!(
|
||||
r"
|
||||
f = \state ->
|
||||
{ state & a: state.b }
|
||||
f { a: 0, b: 0 }
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
#[mono_test]
|
||||
fn issue_6174() {
|
||||
indoc!(
|
||||
r"
|
||||
g = Bool.false
|
||||
|
||||
a = \_ ->
|
||||
if g then
|
||||
Ok 0
|
||||
else
|
||||
Err NoNumber
|
||||
|
||||
b = \_ ->
|
||||
if g then
|
||||
Ok 0
|
||||
else
|
||||
Err NoNumber
|
||||
|
||||
c = \_ ->
|
||||
[a {}, b {}]
|
||||
|
||||
c {}
|
||||
"
|
||||
)
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ shape = \@Types types, id ->
|
||||
Err OutOfBounds ->
|
||||
idStr = Num.toStr (TypeId.toU64 id)
|
||||
|
||||
crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>"
|
||||
crash "TypeId #$(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>"
|
||||
|
||||
alignment : Types, TypeId -> U32
|
||||
alignment = \@Types types, id ->
|
||||
@ -54,7 +54,7 @@ alignment = \@Types types, id ->
|
||||
Err OutOfBounds ->
|
||||
idStr = Num.toStr (TypeId.toU64 id)
|
||||
|
||||
crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>"
|
||||
crash "TypeId #$(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>"
|
||||
|
||||
size : Types, TypeId -> U32
|
||||
size = \@Types types, id ->
|
||||
@ -63,4 +63,4 @@ size = \@Types types, id ->
|
||||
Err OutOfBounds ->
|
||||
idStr = Num.toStr (TypeId.toU64 id)
|
||||
|
||||
crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>"
|
||||
crash "TypeId #$(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at <https://github.com/roc-lang/roc/issues>"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,15 @@ mod glue_cli_run {
|
||||
use crate::helpers::fixtures_dir;
|
||||
use cli_utils::helpers::{has_error, run_glue, run_roc, Out};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
const TEST_LEGACY_LINKER: bool = true;
|
||||
|
||||
// Surgical linker currently only supports linux x86_64,
|
||||
// so we're always testing the legacy linker on other targets.
|
||||
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
|
||||
const TEST_LEGACY_LINKER: bool = false;
|
||||
|
||||
/// This macro does two things.
|
||||
///
|
||||
@ -40,13 +48,8 @@ mod glue_cli_run {
|
||||
|
||||
generate_glue_for(&dir, std::iter::empty());
|
||||
|
||||
let test_name_str = stringify!($test_name);
|
||||
|
||||
// TODO after #5924 is fixed; remove this
|
||||
let skip_on_linux = ["closures", "option", "nullable_wrapped"];
|
||||
|
||||
if !(cfg!(target_os = "linux") && (skip_on_linux.contains(&test_name_str))) {
|
||||
let out = run_app(&dir.join("app.roc"), std::iter::empty());
|
||||
fn validate<'a, I: IntoIterator<Item = &'a str>>(dir: PathBuf, args: I) {
|
||||
let out = run_app(&dir.join("app.roc"), args);
|
||||
|
||||
assert!(out.status.success());
|
||||
let ignorable = "🔨 Rebuilding platform...\n";
|
||||
@ -59,6 +62,21 @@ mod glue_cli_run {
|
||||
out.stdout
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
let test_name_str = stringify!($test_name);
|
||||
|
||||
// TODO after #5924 is fixed; remove this
|
||||
let skip_on_linux_surgical_linker = ["closures", "option", "nullable_wrapped", "enumeration", "nested_record"];
|
||||
|
||||
// Validate linux with the default linker.
|
||||
if !(cfg!(target_os = "linux") && (skip_on_linux_surgical_linker.contains(&test_name_str))) {
|
||||
validate(dir.clone(), std::iter::empty());
|
||||
}
|
||||
|
||||
if TEST_LEGACY_LINKER {
|
||||
validate(dir, ["--linker=legacy"]);
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
@ -237,7 +255,7 @@ mod glue_cli_run {
|
||||
glue_out
|
||||
}
|
||||
|
||||
fn run_app<'a, I: IntoIterator<Item = &'a str>>(app_file: &'a Path, args: I) -> Out {
|
||||
fn run_app<'a, 'b, I: IntoIterator<Item = &'a str>>(app_file: &'b Path, args: I) -> Out {
|
||||
// Generate test_glue for this platform
|
||||
let compile_out = run_roc(
|
||||
// converting these all to String avoids lifetime issues
|
||||
|
@ -7,6 +7,10 @@ edition = "2021"
|
||||
name = "roc_ls"
|
||||
path = "src/server.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
expect-test = "1.4.1"
|
||||
|
||||
|
||||
[dependencies]
|
||||
roc_can = { path = "../compiler/can" }
|
||||
roc_collections = { path = "../compiler/collections" }
|
||||
@ -27,3 +31,6 @@ parking_lot.workspace = true
|
||||
|
||||
tower-lsp = "0.17.0"
|
||||
tokio = { version = "1.20.1", features = [ "rt", "rt-multi-thread", "macros", "io-std" ] }
|
||||
log.workspace = true
|
||||
indoc.workspace=true
|
||||
env_logger = "0.10.1"
|
||||
|
@ -78,3 +78,21 @@ If you're using coc.nvim and want to use the configuration above, be sure to als
|
||||
|
||||
If you want to debug the server, use [debug_server.sh](./debug_server.sh)
|
||||
instead of the direct binary.
|
||||
|
||||
If you would like to enable debug logging set the `ROCLS_LOG` environment variable to `debug` or `trace` for even more logs.
|
||||
eg: `ROCLS_LOG=debug`
|
||||
|
||||
## Testing
|
||||
|
||||
Tests use expect-test, which is a snapshot/expect testing framework.
|
||||
If a change is made that requires updating the expect tests run `cargo test` confirm that the diff is correct, then run `UPDATE_EXPECT=1 cargo test` to update the contents of the files with the new output.
|
||||
|
||||
## Config
|
||||
|
||||
You can set the environment variables below to control the operation of the language.
|
||||
|
||||
`ROCLS_DEBOUNCE_MS`: Sets the amount of time to delay starting analysis of the document when a change comes in. This prevents starting pointless analysis while you are typing normally.
|
||||
Default: `100`
|
||||
|
||||
`ROCLS_LATEST_DOC_TIMEOUT_MS`: Sets the timeout for waiting for an analysis of the latest document to be complete. If a request is sent that needs the latest version of the document to be analyzed, then it will wait up to this duration before just giving up.
|
||||
Default: `5000`
|
||||
|
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd)
|
||||
RUST_LOG=debug
|
||||
${SCRIPT_DIR}/../../target/debug/roc_ls "$@" 2> /tmp/roc_ls.err
|
||||
|
@ -1,116 +1,120 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use bumpalo::Bump;
|
||||
use roc_can::{abilities::AbilitiesStore, expr::Declarations};
|
||||
use roc_collections::MutMap;
|
||||
use roc_load::{CheckedModule, LoadedModule};
|
||||
use roc_module::symbol::{Interns, ModuleId, Symbol};
|
||||
use roc_module::symbol::{Interns, ModuleId};
|
||||
use roc_packaging::cache::{self, RocCacheDir};
|
||||
use roc_region::all::LineInfo;
|
||||
use roc_reporting::report::RocDocAllocator;
|
||||
use roc_solve_problem::TypeError;
|
||||
use roc_types::subs::Subs;
|
||||
use tower_lsp::lsp_types::{
|
||||
Diagnostic, GotoDefinitionResponse, Hover, HoverContents, Location, MarkedString, Position,
|
||||
Range, SemanticTokenType, SemanticTokens, SemanticTokensResult, TextEdit, Url,
|
||||
};
|
||||
|
||||
use crate::convert::{
|
||||
diag::{IntoLspDiagnostic, ProblemFmt},
|
||||
ToRange, ToRocPosition,
|
||||
};
|
||||
use tower_lsp::lsp_types::{Diagnostic, SemanticTokenType, Url};
|
||||
|
||||
mod analysed_doc;
|
||||
mod completion;
|
||||
mod parse_ast;
|
||||
mod semantic_tokens;
|
||||
mod tokens;
|
||||
mod utils;
|
||||
|
||||
use crate::convert::diag::{IntoLspDiagnostic, ProblemFmt};
|
||||
|
||||
pub(crate) use self::analysed_doc::{AnalyzedDocument, DocInfo};
|
||||
use self::{analysed_doc::ModuleIdToUrl, tokens::Token};
|
||||
|
||||
use self::{parse_ast::Ast, semantic_tokens::arrange_semantic_tokens, tokens::Token};
|
||||
pub const HIGHLIGHT_TOKENS_LEGEND: &[SemanticTokenType] = Token::LEGEND;
|
||||
|
||||
pub(crate) struct GlobalAnalysis {
|
||||
pub documents: Vec<AnalyzedDocument>,
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct AnalyzedModule {
|
||||
module_id: ModuleId,
|
||||
interns: Interns,
|
||||
subs: Subs,
|
||||
abilities: AbilitiesStore,
|
||||
declarations: Declarations,
|
||||
// We need this because ModuleIds are not stable between compilations, so a ModuleId visible to
|
||||
// one module may not be true global to the language server.
|
||||
module_id_to_url: ModuleIdToUrl,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnalysisResult {
|
||||
module: Option<AnalyzedModule>,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl GlobalAnalysis {
|
||||
pub fn new(source_url: Url, source: String) -> GlobalAnalysis {
|
||||
let arena = Bump::new();
|
||||
pub(crate) fn global_analysis(doc_info: DocInfo) -> Vec<AnalyzedDocument> {
|
||||
let fi = doc_info.url.to_file_path().unwrap();
|
||||
let src_dir = find_src_dir(&fi).to_path_buf();
|
||||
|
||||
let fi = source_url.to_file_path().unwrap();
|
||||
let src_dir = find_src_dir(&fi).to_path_buf();
|
||||
let line_info = LineInfo::new(&source);
|
||||
let arena = Bump::new();
|
||||
let loaded = roc_load::load_and_typecheck_str(
|
||||
&arena,
|
||||
fi,
|
||||
&doc_info.source,
|
||||
src_dir,
|
||||
roc_target::TargetInfo::default_x86_64(),
|
||||
roc_load::FunctionKind::LambdaSet,
|
||||
roc_reporting::report::RenderTarget::Generic,
|
||||
RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
|
||||
roc_reporting::report::DEFAULT_PALETTE,
|
||||
);
|
||||
|
||||
let loaded = roc_load::load_and_typecheck_str(
|
||||
&arena,
|
||||
fi,
|
||||
&source,
|
||||
src_dir,
|
||||
roc_target::TargetInfo::default_x86_64(),
|
||||
roc_load::FunctionKind::LambdaSet,
|
||||
roc_reporting::report::RenderTarget::Generic,
|
||||
RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
|
||||
roc_reporting::report::DEFAULT_PALETTE,
|
||||
);
|
||||
let module = match loaded {
|
||||
Ok(module) => module,
|
||||
Err(problem) => {
|
||||
let all_problems = problem
|
||||
.into_lsp_diagnostic(&())
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let module = match loaded {
|
||||
Ok(module) => module,
|
||||
Err(problem) => {
|
||||
let all_problems = problem
|
||||
.into_lsp_diagnostic(&())
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let analyzed_document = AnalyzedDocument {
|
||||
url: source_url,
|
||||
line_info,
|
||||
source,
|
||||
let analyzed_document = AnalyzedDocument {
|
||||
doc_info,
|
||||
analysis_result: AnalysisResult {
|
||||
module: None,
|
||||
diagnostics: all_problems,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return GlobalAnalysis {
|
||||
documents: vec![analyzed_document],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let mut documents = vec![];
|
||||
|
||||
let LoadedModule {
|
||||
interns,
|
||||
mut can_problems,
|
||||
mut type_problems,
|
||||
mut declarations_by_id,
|
||||
sources,
|
||||
mut typechecked,
|
||||
solved,
|
||||
abilities_store,
|
||||
..
|
||||
} = module;
|
||||
|
||||
let mut root_module = Some(RootModule {
|
||||
subs: solved.into_inner(),
|
||||
abilities_store,
|
||||
});
|
||||
|
||||
let mut builder = AnalyzedDocumentBuilder {
|
||||
interns: &interns,
|
||||
module_id_to_url: module_id_to_url_from_sources(&sources),
|
||||
can_problems: &mut can_problems,
|
||||
type_problems: &mut type_problems,
|
||||
declarations_by_id: &mut declarations_by_id,
|
||||
typechecked: &mut typechecked,
|
||||
root_module: &mut root_module,
|
||||
};
|
||||
|
||||
for (module_id, (path, source)) in sources {
|
||||
documents.push(builder.build_document(path, source, module_id));
|
||||
return vec![analyzed_document];
|
||||
}
|
||||
};
|
||||
|
||||
GlobalAnalysis { documents }
|
||||
let mut documents = vec![];
|
||||
|
||||
let LoadedModule {
|
||||
interns,
|
||||
mut can_problems,
|
||||
mut type_problems,
|
||||
mut declarations_by_id,
|
||||
sources,
|
||||
mut typechecked,
|
||||
solved,
|
||||
abilities_store,
|
||||
..
|
||||
} = module;
|
||||
|
||||
let mut root_module = Some(RootModule {
|
||||
subs: solved.into_inner(),
|
||||
abilities_store,
|
||||
});
|
||||
|
||||
let mut builder = AnalyzedDocumentBuilder {
|
||||
interns: &interns,
|
||||
module_id_to_url: module_id_to_url_from_sources(&sources),
|
||||
can_problems: &mut can_problems,
|
||||
type_problems: &mut type_problems,
|
||||
declarations_by_id: &mut declarations_by_id,
|
||||
typechecked: &mut typechecked,
|
||||
root_module: &mut root_module,
|
||||
};
|
||||
|
||||
for (module_id, (path, source)) in sources {
|
||||
documents.push(builder.build_document(path, source, module_id, doc_info.version));
|
||||
}
|
||||
|
||||
documents
|
||||
}
|
||||
|
||||
fn find_src_dir(path: &Path) -> &Path {
|
||||
@ -169,6 +173,7 @@ impl<'a> AnalyzedDocumentBuilder<'a> {
|
||||
path: PathBuf,
|
||||
source: Box<str>,
|
||||
module_id: ModuleId,
|
||||
version: i32,
|
||||
) -> AnalyzedDocument {
|
||||
let subs;
|
||||
let abilities;
|
||||
@ -198,11 +203,16 @@ impl<'a> AnalyzedDocumentBuilder<'a> {
|
||||
let diagnostics = self.build_diagnostics(&path, &source, &line_info, module_id);
|
||||
|
||||
AnalyzedDocument {
|
||||
url: path_to_url(&path),
|
||||
line_info,
|
||||
source: source.into(),
|
||||
module: Some(analyzed_module),
|
||||
diagnostics,
|
||||
doc_info: DocInfo {
|
||||
url: path_to_url(&path),
|
||||
line_info,
|
||||
source: source.into(),
|
||||
version,
|
||||
},
|
||||
analysis_result: AnalysisResult {
|
||||
module: Some(analyzed_module),
|
||||
diagnostics,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,157 +253,3 @@ impl<'a> AnalyzedDocumentBuilder<'a> {
|
||||
all_problems
|
||||
}
|
||||
}
|
||||
|
||||
type ModuleIdToUrl = HashMap<ModuleId, Url>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AnalyzedModule {
|
||||
module_id: ModuleId,
|
||||
interns: Interns,
|
||||
subs: Subs,
|
||||
abilities: AbilitiesStore,
|
||||
declarations: Declarations,
|
||||
// We need this because ModuleIds are not stable between compilations, so a ModuleId visible to
|
||||
// one module may not be true global to the language server.
|
||||
module_id_to_url: ModuleIdToUrl,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AnalyzedDocument {
|
||||
url: Url,
|
||||
line_info: LineInfo,
|
||||
source: String,
|
||||
module: Option<AnalyzedModule>,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl AnalyzedDocument {
|
||||
pub fn url(&self) -> &Url {
|
||||
&self.url
|
||||
}
|
||||
|
||||
fn line_info(&self) -> &LineInfo {
|
||||
&self.line_info
|
||||
}
|
||||
|
||||
fn module_mut(&mut self) -> Option<&mut AnalyzedModule> {
|
||||
self.module.as_mut()
|
||||
}
|
||||
|
||||
fn module(&self) -> Option<&AnalyzedModule> {
|
||||
self.module.as_ref()
|
||||
}
|
||||
|
||||
fn location(&self, range: Range) -> Location {
|
||||
Location {
|
||||
uri: self.url.clone(),
|
||||
range,
|
||||
}
|
||||
}
|
||||
|
||||
fn whole_document_range(&self) -> Range {
|
||||
let line_info = self.line_info();
|
||||
let start = Position::new(0, 0);
|
||||
let end = Position::new(line_info.num_lines(), 0);
|
||||
Range::new(start, end)
|
||||
}
|
||||
|
||||
pub fn diagnostics(&mut self) -> Vec<Diagnostic> {
|
||||
self.diagnostics.clone()
|
||||
}
|
||||
|
||||
pub fn symbol_at(&self, position: Position) -> Option<Symbol> {
|
||||
let line_info = self.line_info();
|
||||
|
||||
let position = position.to_roc_position(line_info);
|
||||
|
||||
let AnalyzedModule {
|
||||
declarations,
|
||||
abilities,
|
||||
..
|
||||
} = self.module()?;
|
||||
|
||||
let found_symbol =
|
||||
roc_can::traverse::find_closest_symbol_at(position, declarations, abilities)?;
|
||||
|
||||
Some(found_symbol.implementation_symbol())
|
||||
}
|
||||
|
||||
pub fn hover(&mut self, position: Position) -> Option<Hover> {
|
||||
let line_info = self.line_info();
|
||||
|
||||
let pos = position.to_roc_position(line_info);
|
||||
|
||||
let AnalyzedModule {
|
||||
subs,
|
||||
declarations,
|
||||
module_id,
|
||||
interns,
|
||||
..
|
||||
} = self.module_mut()?;
|
||||
|
||||
let (region, var) = roc_can::traverse::find_closest_type_at(pos, declarations)?;
|
||||
|
||||
let snapshot = subs.snapshot();
|
||||
let type_str = roc_types::pretty_print::name_and_print_var(
|
||||
var,
|
||||
subs,
|
||||
*module_id,
|
||||
interns,
|
||||
roc_types::pretty_print::DebugPrint::NOTHING,
|
||||
);
|
||||
subs.rollback_to(snapshot);
|
||||
|
||||
let range = region.to_range(self.line_info());
|
||||
|
||||
Some(Hover {
|
||||
contents: HoverContents::Scalar(MarkedString::String(type_str)),
|
||||
range: Some(range),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn definition(&self, symbol: Symbol) -> Option<GotoDefinitionResponse> {
|
||||
let AnalyzedModule { declarations, .. } = self.module()?;
|
||||
|
||||
let found_declaration = roc_can::traverse::find_declaration(symbol, declarations)?;
|
||||
|
||||
let range = found_declaration.region().to_range(self.line_info());
|
||||
|
||||
Some(GotoDefinitionResponse::Scalar(self.location(range)))
|
||||
}
|
||||
|
||||
pub fn format(&self) -> Option<Vec<TextEdit>> {
|
||||
let source = &self.source;
|
||||
let arena = &Bump::new();
|
||||
|
||||
let ast = Ast::parse(arena, source).ok()?;
|
||||
let fmt = ast.fmt();
|
||||
|
||||
if source == fmt.as_str() {
|
||||
None
|
||||
} else {
|
||||
let range = self.whole_document_range();
|
||||
let text_edit = TextEdit::new(range, fmt.to_string().to_string());
|
||||
Some(vec![text_edit])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn semantic_tokens(&self) -> Option<SemanticTokensResult> {
|
||||
let source = &self.source;
|
||||
let arena = &Bump::new();
|
||||
|
||||
let ast = Ast::parse(arena, source).ok()?;
|
||||
let tokens = ast.semantic_tokens();
|
||||
|
||||
let data = arrange_semantic_tokens(tokens, &self.line_info);
|
||||
|
||||
Some(SemanticTokensResult::Tokens(SemanticTokens {
|
||||
result_id: None,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn module_url(&self, module_id: ModuleId) -> Option<Url> {
|
||||
self.module()?.module_id_to_url.get(&module_id).cloned()
|
||||
}
|
||||
}
|
||||
|
251
crates/lang_srv/src/analysis/analysed_doc.rs
Normal file
251
crates/lang_srv/src/analysis/analysed_doc.rs
Normal file
@ -0,0 +1,251 @@
|
||||
use log::debug;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bumpalo::Bump;
|
||||
|
||||
use roc_module::symbol::{ModuleId, Symbol};
|
||||
|
||||
use roc_region::all::LineInfo;
|
||||
|
||||
use tower_lsp::lsp_types::{
|
||||
CompletionItem, Diagnostic, GotoDefinitionResponse, Hover, HoverContents, LanguageString,
|
||||
Location, MarkedString, Position, Range, SemanticTokens, SemanticTokensResult, TextEdit, Url,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
analysis::completion::{field_completion, get_completion_items},
|
||||
convert::{ToRange, ToRocPosition},
|
||||
};
|
||||
|
||||
use super::{
|
||||
parse_ast::Ast,
|
||||
semantic_tokens::arrange_semantic_tokens,
|
||||
utils::{format_var_type, is_roc_identifier_char},
|
||||
AnalysisResult, AnalyzedModule,
|
||||
};
|
||||
|
||||
pub(super) type ModuleIdToUrl = HashMap<ModuleId, Url>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnalyzedDocument {
|
||||
pub doc_info: DocInfo,
|
||||
pub analysis_result: AnalysisResult,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DocInfo {
|
||||
pub url: Url,
|
||||
pub line_info: LineInfo,
|
||||
pub source: String,
|
||||
pub version: i32,
|
||||
}
|
||||
impl DocInfo {
|
||||
pub fn new(url: Url, source: String, version: i32) -> Self {
|
||||
Self {
|
||||
url,
|
||||
line_info: LineInfo::new(&source),
|
||||
source,
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[allow(unused)]
|
||||
fn debug_log_prefix(&self, offset: usize) {
|
||||
debug!("Prefix source: {:?}", self.source);
|
||||
|
||||
let last_few = self.source.get(offset - 5..offset + 5).unwrap();
|
||||
|
||||
let (before, after) = last_few.split_at(5);
|
||||
|
||||
debug!(
|
||||
"Starting to get completion items at offset: {:?} content: '{:?}|{:?}'",
|
||||
offset, before, after
|
||||
);
|
||||
}
|
||||
|
||||
fn whole_document_range(&self) -> Range {
|
||||
let start = Position::new(0, 0);
|
||||
let end = Position::new(self.line_info.num_lines(), 0);
|
||||
Range::new(start, end)
|
||||
}
|
||||
pub fn get_prefix_at_position(&self, position: Position) -> String {
|
||||
let position = position.to_roc_position(&self.line_info);
|
||||
let offset = position.offset as usize;
|
||||
let source = &self.source.as_bytes()[..offset];
|
||||
let symbol_len = source
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|&a| is_roc_identifier_char(&(*a as char)))
|
||||
.count();
|
||||
let symbol = &self.source[offset - symbol_len..offset];
|
||||
|
||||
String::from(symbol)
|
||||
}
|
||||
pub fn format(&self) -> Option<Vec<TextEdit>> {
|
||||
let source = &self.source;
|
||||
let arena = &Bump::new();
|
||||
|
||||
let ast = Ast::parse(arena, source).ok()?;
|
||||
let fmt = ast.fmt();
|
||||
|
||||
if source == fmt.as_str() {
|
||||
None
|
||||
} else {
|
||||
let range = self.whole_document_range();
|
||||
let text_edit = TextEdit::new(range, fmt.to_string().to_string());
|
||||
Some(vec![text_edit])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn semantic_tokens(&self) -> Option<SemanticTokensResult> {
|
||||
let source = &self.source;
|
||||
let arena = &Bump::new();
|
||||
|
||||
let ast = Ast::parse(arena, source).ok()?;
|
||||
let tokens = ast.semantic_tokens();
|
||||
|
||||
let data = arrange_semantic_tokens(tokens, &self.line_info);
|
||||
|
||||
Some(SemanticTokensResult::Tokens(SemanticTokens {
|
||||
result_id: None,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl AnalyzedDocument {
|
||||
pub fn url(&self) -> &Url {
|
||||
&self.doc_info.url
|
||||
}
|
||||
|
||||
fn line_info(&self) -> &LineInfo {
|
||||
&self.doc_info.line_info
|
||||
}
|
||||
|
||||
fn module(&self) -> Option<&AnalyzedModule> {
|
||||
self.analysis_result.module.as_ref()
|
||||
}
|
||||
|
||||
fn location(&self, range: Range) -> Location {
|
||||
Location {
|
||||
uri: self.doc_info.url.clone(),
|
||||
range,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_checked(&self) -> bool {
|
||||
self.analysis_result.module.is_some()
|
||||
}
|
||||
|
||||
pub fn diagnostics(&self) -> Vec<Diagnostic> {
|
||||
self.analysis_result.diagnostics.clone()
|
||||
}
|
||||
|
||||
pub fn symbol_at(&self, position: Position) -> Option<Symbol> {
|
||||
let line_info = self.line_info();
|
||||
|
||||
let position = position.to_roc_position(line_info);
|
||||
|
||||
let AnalyzedModule {
|
||||
declarations,
|
||||
abilities,
|
||||
..
|
||||
} = self.module()?;
|
||||
|
||||
let found_symbol =
|
||||
roc_can::traverse::find_closest_symbol_at(position, declarations, abilities)?;
|
||||
|
||||
Some(found_symbol.implementation_symbol())
|
||||
}
|
||||
|
||||
pub fn hover(&self, position: Position) -> Option<Hover> {
|
||||
let line_info = self.line_info();
|
||||
|
||||
let pos = position.to_roc_position(line_info);
|
||||
|
||||
let AnalyzedModule {
|
||||
subs,
|
||||
declarations,
|
||||
module_id,
|
||||
interns,
|
||||
..
|
||||
} = self.module()?;
|
||||
|
||||
let (region, var) = roc_can::traverse::find_closest_type_at(pos, declarations)?;
|
||||
let type_str = format_var_type(var, &mut subs.clone(), module_id, interns);
|
||||
|
||||
let range = region.to_range(self.line_info());
|
||||
|
||||
Some(Hover {
|
||||
contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString {
|
||||
language: "roc".to_string(),
|
||||
value: type_str,
|
||||
})),
|
||||
range: Some(range),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn definition(&self, symbol: Symbol) -> Option<GotoDefinitionResponse> {
|
||||
let AnalyzedModule { declarations, .. } = self.module()?;
|
||||
|
||||
let found_declaration = roc_can::traverse::find_declaration(symbol, declarations)?;
|
||||
|
||||
let range = found_declaration.region().to_range(self.line_info());
|
||||
|
||||
Some(GotoDefinitionResponse::Scalar(self.location(range)))
|
||||
}
|
||||
|
||||
pub(crate) fn module_url(&self, module_id: ModuleId) -> Option<Url> {
|
||||
self.module()?.module_id_to_url.get(&module_id).cloned()
|
||||
}
|
||||
|
||||
pub fn completion_items(
|
||||
&self,
|
||||
position: Position,
|
||||
latest_doc: &DocInfo,
|
||||
) -> Option<Vec<CompletionItem>> {
|
||||
let symbol_prefix = latest_doc.get_prefix_at_position(position);
|
||||
debug!(
|
||||
"Starting to get completion items for prefix: {:?} docVersion:{:?}",
|
||||
symbol_prefix, latest_doc.version
|
||||
);
|
||||
let len_diff = latest_doc.source.len() as i32 - self.doc_info.source.len() as i32;
|
||||
|
||||
//We offset the position because we need the position to be in the correct scope in the most recently parsed version of the source. The quick and dirty method is to just remove the difference in length between the source files from the offset. This could cause issues, but is very easy
|
||||
//TODO: this is kind of a hack and should be removed once we can do some minimal parsing without full type checking
|
||||
let mut position = position.to_roc_position(&latest_doc.line_info);
|
||||
position.offset = (position.offset as i32 - len_diff - 1) as u32;
|
||||
debug!("Completion offset: {:?}", position.offset);
|
||||
|
||||
let AnalyzedModule {
|
||||
module_id,
|
||||
interns,
|
||||
subs,
|
||||
declarations,
|
||||
..
|
||||
} = self.module()?;
|
||||
|
||||
let is_field_completion = symbol_prefix.contains('.');
|
||||
if is_field_completion {
|
||||
field_completion(
|
||||
position,
|
||||
symbol_prefix,
|
||||
declarations,
|
||||
interns,
|
||||
&mut subs.clone(),
|
||||
module_id,
|
||||
)
|
||||
} else {
|
||||
let completions = get_completion_items(
|
||||
position,
|
||||
symbol_prefix,
|
||||
declarations,
|
||||
&mut subs.clone(),
|
||||
module_id,
|
||||
interns,
|
||||
);
|
||||
Some(completions)
|
||||
}
|
||||
}
|
||||
}
|
443
crates/lang_srv/src/analysis/completion.rs
Normal file
443
crates/lang_srv/src/analysis/completion.rs
Normal file
@ -0,0 +1,443 @@
|
||||
use log::{debug, trace, warn};
|
||||
use roc_can::{
|
||||
def::Def,
|
||||
expr::{ClosureData, Declarations, Expr, WhenBranch},
|
||||
pattern::{ListPatterns, Pattern, RecordDestruct, TupleDestruct},
|
||||
traverse::{walk_decl, walk_def, walk_expr, DeclarationInfo, Visitor},
|
||||
};
|
||||
use roc_module::symbol::{Interns, ModuleId, Symbol};
|
||||
use roc_region::all::{Loc, Position, Region};
|
||||
use roc_types::subs::{Subs, Variable};
|
||||
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind};
|
||||
|
||||
use super::utils::format_var_type;
|
||||
|
||||
pub struct CompletionVisitor<'a> {
|
||||
position: Position,
|
||||
found_decls: Vec<(Symbol, Variable)>,
|
||||
pub interns: &'a Interns,
|
||||
pub prefix: String,
|
||||
}
|
||||
|
||||
impl Visitor for CompletionVisitor<'_> {
|
||||
fn should_visit(&mut self, region: Region) -> bool {
|
||||
region.contains_pos(self.position)
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) {
|
||||
if region.contains_pos(self.position) {
|
||||
let mut res = self.expression_defs(expr);
|
||||
self.found_decls.append(&mut res);
|
||||
|
||||
walk_expr(self, expr, var);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_decl(&mut self, decl: DeclarationInfo<'_>) {
|
||||
match decl {
|
||||
DeclarationInfo::Value { loc_expr, .. }
|
||||
| DeclarationInfo::Function {
|
||||
loc_body: loc_expr, ..
|
||||
}
|
||||
| DeclarationInfo::Destructure { loc_expr, .. } => {
|
||||
let res = self.decl_to_completion_item(&decl);
|
||||
self.found_decls.extend(res);
|
||||
if loc_expr.region.contains_pos(self.position) {
|
||||
walk_decl(self, decl);
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
walk_decl(self, decl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_def(&mut self, def: &Def) {
|
||||
let res = self.extract_defs(def);
|
||||
self.found_decls.extend(res);
|
||||
walk_def(self, def);
|
||||
}
|
||||
}
|
||||
impl CompletionVisitor<'_> {
|
||||
fn extract_defs(&mut self, def: &Def) -> Vec<(Symbol, Variable)> {
|
||||
trace!("Completion begin");
|
||||
def.pattern_vars
|
||||
.iter()
|
||||
.map(|(symbol, var)| (*symbol, *var))
|
||||
.collect()
|
||||
}
|
||||
fn expression_defs(&self, expr: &Expr) -> Vec<(Symbol, Variable)> {
|
||||
match expr {
|
||||
Expr::When {
|
||||
expr_var, branches, ..
|
||||
} => self.when_is_expr(branches, expr_var),
|
||||
Expr::Closure(ClosureData {
|
||||
arguments,
|
||||
loc_body,
|
||||
..
|
||||
}) => {
|
||||
//if we are inside the closure complete it's vars
|
||||
if loc_body.region.contains_pos(self.position) {
|
||||
arguments
|
||||
.iter()
|
||||
.flat_map(|(var, _, pat)| self.patterns(&pat.value, var))
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
///Extract any variables made available by the branch of a when_is expression that contains `self.position`
|
||||
fn when_is_expr(
|
||||
&self,
|
||||
branches: &[WhenBranch],
|
||||
expr_var: &Variable,
|
||||
) -> Vec<(Symbol, Variable)> {
|
||||
branches
|
||||
.iter()
|
||||
.flat_map(
|
||||
|WhenBranch {
|
||||
patterns, value, ..
|
||||
}| {
|
||||
if value.region.contains_pos(self.position) {
|
||||
patterns
|
||||
.iter()
|
||||
.flat_map(|pattern| self.patterns(&pattern.pattern.value, expr_var))
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn record_destructure(&self, destructs: &[Loc<RecordDestruct>]) -> Vec<(Symbol, Variable)> {
|
||||
destructs
|
||||
.iter()
|
||||
.flat_map(|a| match &a.value.typ {
|
||||
roc_can::pattern::DestructType::Required
|
||||
| roc_can::pattern::DestructType::Optional(_, _) => {
|
||||
vec![(a.value.symbol, a.value.var)]
|
||||
}
|
||||
roc_can::pattern::DestructType::Guard(var, pat) => self.patterns(&pat.value, var),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tuple_destructure(&self, destructs: &[Loc<TupleDestruct>]) -> Vec<(Symbol, Variable)> {
|
||||
destructs
|
||||
.iter()
|
||||
.flat_map(|a| {
|
||||
let (var, pattern) = &a.value.typ;
|
||||
self.patterns(&pattern.value, var)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn list_pattern(&self, list_elems: &ListPatterns, var: &Variable) -> Vec<(Symbol, Variable)> {
|
||||
list_elems
|
||||
.patterns
|
||||
.iter()
|
||||
.flat_map(|a| self.patterns(&a.value, var))
|
||||
.collect()
|
||||
}
|
||||
fn tag_pattern(&self, arguments: &[(Variable, Loc<Pattern>)]) -> Vec<(Symbol, Variable)> {
|
||||
arguments
|
||||
.iter()
|
||||
.flat_map(|(var, pat)| self.patterns(&pat.value, var))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn as_pattern(
|
||||
&self,
|
||||
as_pat: &Pattern,
|
||||
as_symbol: Symbol,
|
||||
var: &Variable,
|
||||
) -> Vec<(Symbol, Variable)> {
|
||||
//Get the variables introduced within the pattern
|
||||
let mut patterns = self.patterns(as_pat, var);
|
||||
//Add the "as" that wraps the whole pattern
|
||||
patterns.push((as_symbol, *var));
|
||||
patterns
|
||||
}
|
||||
///Returns a list of symbols defined by this pattern.
|
||||
///`pattern_var`: Variable type of the entire pattern. This will be returned if the pattern turns out to be an identifier
|
||||
fn patterns(
|
||||
&self,
|
||||
pattern: &roc_can::pattern::Pattern,
|
||||
pattern_var: &Variable,
|
||||
) -> Vec<(Symbol, Variable)> {
|
||||
match pattern {
|
||||
roc_can::pattern::Pattern::Identifier(symbol) => {
|
||||
if self.is_match(symbol) {
|
||||
vec![(*symbol, *pattern_var)]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
Pattern::AppliedTag { arguments, .. } => self.tag_pattern(arguments),
|
||||
Pattern::UnwrappedOpaque { argument, .. } => {
|
||||
self.patterns(&argument.1.value, &argument.0)
|
||||
}
|
||||
Pattern::List {
|
||||
elem_var, patterns, ..
|
||||
} => self.list_pattern(patterns, elem_var),
|
||||
roc_can::pattern::Pattern::As(pat, symbol) => {
|
||||
self.as_pattern(&pat.value, *symbol, pattern_var)
|
||||
}
|
||||
roc_can::pattern::Pattern::RecordDestructure { destructs, .. } => {
|
||||
self.record_destructure(destructs)
|
||||
}
|
||||
roc_can::pattern::Pattern::TupleDestructure { destructs, .. } => {
|
||||
self.tuple_destructure(destructs)
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn is_match(&self, symbol: &Symbol) -> bool {
|
||||
symbol.as_str(self.interns).starts_with(&self.prefix)
|
||||
}
|
||||
|
||||
fn decl_to_completion_item(&self, decl: &DeclarationInfo) -> Vec<(Symbol, Variable)> {
|
||||
match decl {
|
||||
DeclarationInfo::Value {
|
||||
expr_var, pattern, ..
|
||||
} => self.patterns(pattern, expr_var),
|
||||
DeclarationInfo::Function {
|
||||
expr_var,
|
||||
pattern,
|
||||
function,
|
||||
loc_body,
|
||||
..
|
||||
} => {
|
||||
let mut out = vec![];
|
||||
//Append the function declaration itself for recursive calls
|
||||
out.extend(self.patterns(pattern, expr_var));
|
||||
|
||||
if loc_body.region.contains_pos(self.position) {
|
||||
//also add the arguments if we are inside the function
|
||||
let args = function
|
||||
.value
|
||||
.arguments
|
||||
.iter()
|
||||
.flat_map(|(var, _, pat)| self.patterns(&pat.value, var));
|
||||
//We add in the pattern for the function declaration
|
||||
out.extend(args);
|
||||
trace!("Added function args to completion output =:{:#?}", out);
|
||||
}
|
||||
out
|
||||
}
|
||||
DeclarationInfo::Destructure {
|
||||
loc_pattern,
|
||||
expr_var,
|
||||
..
|
||||
} => self.patterns(&loc_pattern.value, expr_var),
|
||||
DeclarationInfo::Expectation { .. } => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_completions(
|
||||
position: Position,
|
||||
decls: &Declarations,
|
||||
prefix: String,
|
||||
interns: &Interns,
|
||||
) -> Vec<(Symbol, Variable)> {
|
||||
let mut visitor = CompletionVisitor {
|
||||
position,
|
||||
found_decls: Vec::new(),
|
||||
interns,
|
||||
prefix,
|
||||
};
|
||||
visitor.visit_decls(decls);
|
||||
visitor.found_decls
|
||||
}
|
||||
|
||||
fn make_completion_item(
|
||||
subs: &mut Subs,
|
||||
module_id: &ModuleId,
|
||||
interns: &Interns,
|
||||
str: String,
|
||||
var: Variable,
|
||||
) -> CompletionItem {
|
||||
let type_str = format_var_type(var, subs, module_id, interns);
|
||||
let typ = match subs.get(var).content {
|
||||
roc_types::subs::Content::Structure(var) => match var {
|
||||
roc_types::subs::FlatType::Apply(_, _) => CompletionItemKind::FUNCTION,
|
||||
roc_types::subs::FlatType::Func(_, _, _) => CompletionItemKind::FUNCTION,
|
||||
roc_types::subs::FlatType::EmptyTagUnion
|
||||
| roc_types::subs::FlatType::TagUnion(_, _) => CompletionItemKind::ENUM,
|
||||
_ => CompletionItemKind::VARIABLE,
|
||||
},
|
||||
a => {
|
||||
debug!(
|
||||
"No specific completionKind for variable type: {:?} defaulting to 'Variable'",
|
||||
a
|
||||
);
|
||||
CompletionItemKind::VARIABLE
|
||||
}
|
||||
};
|
||||
|
||||
CompletionItem {
|
||||
label: str,
|
||||
detail: Some(type_str),
|
||||
kind: Some(typ),
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
/// Walks through declarations that would be accessible from the provided position adding them to a list of completion items until all accessible declarations have been fully explored
|
||||
pub fn get_completion_items(
|
||||
position: Position,
|
||||
prefix: String,
|
||||
decls: &Declarations,
|
||||
subs: &mut Subs,
|
||||
module_id: &ModuleId,
|
||||
interns: &Interns,
|
||||
) -> Vec<CompletionItem> {
|
||||
let completions = get_completions(position, decls, prefix, interns);
|
||||
make_completion_items(
|
||||
subs,
|
||||
module_id,
|
||||
interns,
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|(symb, var)| (symb.as_str(interns).to_string(), var))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
fn make_completion_items(
|
||||
subs: &mut Subs,
|
||||
module_id: &ModuleId,
|
||||
interns: &Interns,
|
||||
completions: Vec<(String, Variable)>,
|
||||
) -> Vec<CompletionItem> {
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|(symbol, var)| make_completion_item(subs, module_id, interns, symbol, var))
|
||||
.collect()
|
||||
}
|
||||
|
||||
///Finds the types of and names of all the fields of a record
|
||||
///`var` should be a `Variable` that you know is a record's type or else it will return an empty list
|
||||
fn find_record_fields(var: Variable, subs: &mut Subs) -> Vec<(String, Variable)> {
|
||||
let content = subs.get(var);
|
||||
match content.content {
|
||||
roc_types::subs::Content::Structure(typ) => match typ {
|
||||
roc_types::subs::FlatType::Record(fields, ext) => {
|
||||
let field_types = fields.unsorted_iterator(subs, ext);
|
||||
|
||||
match field_types {
|
||||
Ok(field) => field
|
||||
.map(|a| (a.0.clone().into(), a.1.into_inner()))
|
||||
.collect::<Vec<_>>(),
|
||||
Err(err) => {
|
||||
warn!("Error getting record field types for completion: {:?}", err);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
roc_types::subs::FlatType::Tuple(elems, ext) => {
|
||||
let elems = elems.unsorted_iterator(subs, ext);
|
||||
|
||||
match elems {
|
||||
Ok(elem) => elem.map(|(num, var)| (num.to_string(), var)).collect(),
|
||||
Err(err) => {
|
||||
warn!("Error getting tuple elems for completion: {:?}", err);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!(
|
||||
"Trying to get field completion for a type that is not a record: {:?}",
|
||||
typ
|
||||
);
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
roc_types::subs::Content::Error => {
|
||||
//This is caused by typechecking our partially typed variable name causing the typechecking to be confused as the type of the parent variable
|
||||
//TODO! ideally i could recover using some previous typecheck result that isn't broken
|
||||
warn!("Variable type of record was of type 'error', cannot access field",);
|
||||
vec![]
|
||||
}
|
||||
_ => {
|
||||
warn!(
|
||||
"Variable before field was unsupported type: {:?}",
|
||||
subs.dbg(var)
|
||||
);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FieldCompletion {
|
||||
var: String,
|
||||
field: String,
|
||||
middle_fields: Vec<String>,
|
||||
}
|
||||
///Splits a completion prefix for a field into its components
|
||||
///E.g. a.b.c.d->{var:"a",middle_fields:["b","c"],field:"d"}
|
||||
fn get_field_completion_parts(symbol_prefix: &str) -> Option<FieldCompletion> {
|
||||
let mut parts = symbol_prefix.split('.').collect::<Vec<_>>();
|
||||
let field = parts.pop().unwrap_or("").to_string();
|
||||
let var = parts.remove(0);
|
||||
//Now that we have the head and tail removed this is all the intermediate fields
|
||||
let middle_fields = parts.into_iter().map(ToString::to_string).collect();
|
||||
|
||||
Some(FieldCompletion {
|
||||
var: var.to_string(),
|
||||
field,
|
||||
middle_fields,
|
||||
})
|
||||
}
|
||||
pub fn field_completion(
|
||||
position: Position,
|
||||
symbol_prefix: String,
|
||||
declarations: &Declarations,
|
||||
interns: &Interns,
|
||||
subs: &mut Subs,
|
||||
module_id: &ModuleId,
|
||||
) -> Option<Vec<CompletionItem>> {
|
||||
let FieldCompletion {
|
||||
var,
|
||||
field,
|
||||
middle_fields,
|
||||
} = get_field_completion_parts(&symbol_prefix)?;
|
||||
|
||||
debug!(
|
||||
"Getting record field completions: variable: {:?} field: {:?} middle: {:?} ",
|
||||
var, field, middle_fields
|
||||
);
|
||||
|
||||
let completion = get_completions(position, declarations, var.to_string(), interns)
|
||||
.into_iter()
|
||||
.map(|a| (a.0.as_str(interns).to_string(), a.1))
|
||||
.next()?;
|
||||
|
||||
//If we have a type that has nested records we could have a completion prefix like: "var.field1.field2.fi"
|
||||
//If the document isn't fully typechecked we won't know what the type of field2 is for us to offer completions based on it's fields
|
||||
//Instead we get the type of "var" and then the type of "field1" within var's type and then "field2" within field1's type etc etc, until we have the type of the record we are actually looking for field completions for.
|
||||
let completion_record = middle_fields.iter().fold(completion, |state, chain_field| {
|
||||
let fields_vars = find_record_fields(state.1, subs);
|
||||
fields_vars
|
||||
.into_iter()
|
||||
.find(|type_field| chain_field == &type_field.0)
|
||||
.unwrap_or(state)
|
||||
});
|
||||
|
||||
let field_completions: Vec<_> = find_record_fields(completion_record.1, subs)
|
||||
.into_iter()
|
||||
.filter(|(str, _)| str.starts_with(&field.to_string()))
|
||||
.collect();
|
||||
|
||||
let field_completions = make_completion_items(subs, module_id, interns, field_completions);
|
||||
Some(field_completions)
|
||||
}
|
24
crates/lang_srv/src/analysis/utils.rs
Normal file
24
crates/lang_srv/src/analysis/utils.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use roc_module::symbol::{Interns, ModuleId};
|
||||
use roc_types::subs::{Subs, Variable};
|
||||
|
||||
pub(super) fn format_var_type(
|
||||
var: Variable,
|
||||
subs: &mut Subs,
|
||||
module_id: &ModuleId,
|
||||
interns: &Interns,
|
||||
) -> String {
|
||||
let snapshot = subs.snapshot();
|
||||
let type_str = roc_types::pretty_print::name_and_print_var(
|
||||
var,
|
||||
subs,
|
||||
*module_id,
|
||||
interns,
|
||||
roc_types::pretty_print::DebugPrint::NOTHING,
|
||||
);
|
||||
subs.rollback_to(snapshot);
|
||||
type_str
|
||||
}
|
||||
|
||||
pub(super) fn is_roc_identifier_char(char: &char) -> bool {
|
||||
matches!(char,'a'..='z'|'A'..='Z'|'0'..='9'|'.')
|
||||
}
|
@ -1,75 +1,219 @@
|
||||
use std::collections::HashMap;
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use tower_lsp::lsp_types::{
|
||||
Diagnostic, GotoDefinitionResponse, Hover, Position, SemanticTokensResult, TextEdit, Url,
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::analysis::{AnalyzedDocument, GlobalAnalysis};
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub(crate) enum DocumentChange {
|
||||
Modified(Url, String),
|
||||
Closed(Url),
|
||||
use tower_lsp::lsp_types::{
|
||||
CompletionResponse, Diagnostic, GotoDefinitionResponse, Hover, Position, SemanticTokensResult,
|
||||
TextEdit, Url,
|
||||
};
|
||||
|
||||
use crate::analysis::{AnalyzedDocument, DocInfo};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DocumentPair {
|
||||
info: DocInfo,
|
||||
latest_document: OnceLock<Arc<AnalyzedDocument>>,
|
||||
last_good_document: Arc<AnalyzedDocument>,
|
||||
}
|
||||
|
||||
impl DocumentPair {
|
||||
pub(crate) fn new(
|
||||
latest_doc: Arc<AnalyzedDocument>,
|
||||
last_good_document: Arc<AnalyzedDocument>,
|
||||
) -> Self {
|
||||
Self {
|
||||
info: latest_doc.doc_info.clone(),
|
||||
latest_document: OnceLock::from(latest_doc),
|
||||
last_good_document,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RegistryConfig {
|
||||
pub(crate) latest_document_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for RegistryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
latest_document_timeout: Duration::from_millis(5000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Registry {
|
||||
documents: HashMap<Url, AnalyzedDocument>,
|
||||
documents: Mutex<HashMap<Url, DocumentPair>>,
|
||||
config: RegistryConfig,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn apply_change(&mut self, change: DocumentChange) {
|
||||
match change {
|
||||
DocumentChange::Modified(url, source) => {
|
||||
let GlobalAnalysis { documents } = GlobalAnalysis::new(url, source);
|
||||
pub(crate) fn new(config: RegistryConfig) -> Self {
|
||||
Self {
|
||||
documents: Default::default(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
// Only replace the set of documents and all dependencies that were re-analyzed.
|
||||
// Note that this is actually the opposite of what we want - in truth we want to
|
||||
// re-evaluate all dependents!
|
||||
for document in documents {
|
||||
let url = document.url().clone();
|
||||
self.documents.insert(url.clone(), document);
|
||||
pub async fn get_latest_version(&self, url: &Url) -> Option<i32> {
|
||||
self.documents.lock().await.get(url).map(|x| x.info.version)
|
||||
}
|
||||
|
||||
fn update_document(
|
||||
documents: &mut MutexGuard<'_, HashMap<Url, DocumentPair>>,
|
||||
document: Arc<AnalyzedDocument>,
|
||||
updating_url: &Url,
|
||||
) {
|
||||
if &document.doc_info.url == updating_url {
|
||||
//Write the newly analysed document into the oncelock that any request requiring the latest document will be waiting on
|
||||
if let Some(a) = documents.get_mut(updating_url) {
|
||||
a.latest_document.set(document.clone()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
let url = document.url().clone();
|
||||
match documents.get_mut(&url) {
|
||||
Some(old_doc) => {
|
||||
//If the latest doc_info has a version higher than what we are setting we shouldn't overwrite the document, but we can update the last_good_document if the parse went well
|
||||
if old_doc.info.version > document.doc_info.version {
|
||||
if document.type_checked() {
|
||||
*old_doc = DocumentPair {
|
||||
info: old_doc.info.clone(),
|
||||
latest_document: old_doc.latest_document.clone(),
|
||||
last_good_document: document,
|
||||
};
|
||||
}
|
||||
} else if document.type_checked() {
|
||||
*old_doc = DocumentPair::new(document.clone(), document);
|
||||
} else {
|
||||
debug!(
|
||||
"Document typechecking failed at version {:?}, not updating last_good_document",
|
||||
&document.doc_info.version
|
||||
);
|
||||
*old_doc = DocumentPair::new(document, old_doc.last_good_document.clone());
|
||||
}
|
||||
}
|
||||
DocumentChange::Closed(_url) => {
|
||||
// Do nothing.
|
||||
None => {
|
||||
documents.insert(url.clone(), DocumentPair::new(document.clone(), document));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn document_by_url(&mut self, url: &Url) -> Option<&mut AnalyzedDocument> {
|
||||
self.documents.get_mut(url)
|
||||
pub async fn apply_changes<'a>(&self, analysed_docs: Vec<AnalyzedDocument>, updating_url: Url) {
|
||||
let mut documents = self.documents.lock().await;
|
||||
debug!(
|
||||
"Finished doc analysis for doc: {}",
|
||||
updating_url.to_string()
|
||||
);
|
||||
|
||||
for document in analysed_docs {
|
||||
let document = Arc::new(document);
|
||||
Registry::update_document(&mut documents, document, &updating_url);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diagnostics(&mut self, url: &Url) -> Vec<Diagnostic> {
|
||||
let Some(document) = self.document_by_url(url) else {
|
||||
pub async fn apply_doc_info_changes(&self, url: Url, info: DocInfo) {
|
||||
let mut documents_lock = self.documents.lock().await;
|
||||
let doc = documents_lock.get_mut(&url);
|
||||
match doc {
|
||||
Some(a) => {
|
||||
debug!(
|
||||
"Set the docInfo for {:?} to version:{:?}",
|
||||
url.as_str(),
|
||||
info.version
|
||||
);
|
||||
*a = DocumentPair {
|
||||
info,
|
||||
last_good_document: a.last_good_document.clone(),
|
||||
latest_document: OnceLock::new(),
|
||||
};
|
||||
}
|
||||
None => debug!("So existing docinfo for {:?} ", url.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn document_info_by_url(&self, url: &Url) -> Option<DocInfo> {
|
||||
self.documents.lock().await.get(url).map(|a| a.info.clone())
|
||||
}
|
||||
|
||||
///Tries to get the latest document from analysis.
|
||||
///Gives up and returns none after 5 seconds.
|
||||
async fn latest_document_by_url(&self, url: &Url) -> Option<Arc<AnalyzedDocument>> {
|
||||
tokio::time::timeout(self.config.latest_document_timeout, async {
|
||||
//TODO: This should really be a condvar that is triggered by the latest being ready, this will do for now though
|
||||
loop {
|
||||
let docs = self.documents.lock().await;
|
||||
if let Some(a) = docs.get(url) {
|
||||
if let Some(a) = a.latest_document.get() {
|
||||
return a.clone();
|
||||
}
|
||||
}
|
||||
drop(docs);
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn diagnostics(&self, url: &Url) -> Vec<Diagnostic> {
|
||||
let Some(document) = self.latest_document_by_url(url).await else {
|
||||
return vec![];
|
||||
};
|
||||
document.diagnostics()
|
||||
}
|
||||
|
||||
pub fn hover(&mut self, url: &Url, position: Position) -> Option<Hover> {
|
||||
self.document_by_url(url)?.hover(position)
|
||||
pub async fn hover(&self, url: &Url, position: Position) -> Option<Hover> {
|
||||
self.latest_document_by_url(url).await?.hover(position)
|
||||
}
|
||||
|
||||
pub fn goto_definition(
|
||||
&mut self,
|
||||
pub async fn goto_definition(
|
||||
&self,
|
||||
url: &Url,
|
||||
position: Position,
|
||||
) -> Option<GotoDefinitionResponse> {
|
||||
let document = self.document_by_url(url)?;
|
||||
let document = self.latest_document_by_url(url).await?;
|
||||
let symbol = document.symbol_at(position)?;
|
||||
let def_document_url = document.module_url(symbol.module_id())?;
|
||||
let def_document = self.document_by_url(&def_document_url)?;
|
||||
let def_document = self.latest_document_by_url(&def_document_url).await?;
|
||||
def_document.definition(symbol)
|
||||
}
|
||||
|
||||
pub fn formatting(&mut self, url: &Url) -> Option<Vec<TextEdit>> {
|
||||
let document = self.document_by_url(url)?;
|
||||
pub async fn formatting(&self, url: &Url) -> Option<Vec<TextEdit>> {
|
||||
let document = self.document_info_by_url(url).await?;
|
||||
document.format()
|
||||
}
|
||||
|
||||
pub fn semantic_tokens(&mut self, url: &Url) -> Option<SemanticTokensResult> {
|
||||
let document = self.document_by_url(url)?;
|
||||
pub async fn semantic_tokens(&self, url: &Url) -> Option<SemanticTokensResult> {
|
||||
let document = self.document_info_by_url(url).await?;
|
||||
document.semantic_tokens()
|
||||
}
|
||||
pub async fn completion_items(
|
||||
&self,
|
||||
url: &Url,
|
||||
position: Position,
|
||||
) -> Option<CompletionResponse> {
|
||||
trace!("Starting completion ");
|
||||
let lock = self.documents.lock().await;
|
||||
let pair = lock.get(url)?;
|
||||
|
||||
let latest_doc_info = &pair.info;
|
||||
info!(
|
||||
"Using document version:{:?} for completion ",
|
||||
latest_doc_info.version
|
||||
);
|
||||
|
||||
let completions = pair
|
||||
.last_good_document
|
||||
.completion_items(position, latest_doc_info)?;
|
||||
|
||||
Some(CompletionResponse::Array(completions))
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,65 @@
|
||||
use analysis::HIGHLIGHT_TOKENS_LEGEND;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use registry::{DocumentChange, Registry};
|
||||
|
||||
use log::{debug, trace};
|
||||
use registry::{Registry, RegistryConfig};
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
use tower_lsp::jsonrpc::Result;
|
||||
use tower_lsp::lsp_types::*;
|
||||
use tower_lsp::{Client, LanguageServer, LspService, Server};
|
||||
|
||||
use crate::analysis::{global_analysis, DocInfo};
|
||||
|
||||
mod analysis;
|
||||
mod convert;
|
||||
mod registry;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RocLs {
|
||||
struct RocServer {
|
||||
pub state: RocServerState,
|
||||
client: Client,
|
||||
registry: Mutex<Registry>,
|
||||
}
|
||||
|
||||
impl std::panic::RefUnwindSafe for RocLs {}
|
||||
struct RocServerConfig {
|
||||
pub debounce_ms: Duration,
|
||||
}
|
||||
|
||||
impl RocLs {
|
||||
pub fn new(client: Client) -> Self {
|
||||
impl Default for RocServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
client,
|
||||
registry: Mutex::new(Registry::default()),
|
||||
debounce_ms: Duration::from_millis(100),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn registry(&self) -> MutexGuard<Registry> {
|
||||
self.registry.lock()
|
||||
///This exists so we can test most of RocLs without anything LSP related
|
||||
struct RocServerState {
|
||||
registry: Registry,
|
||||
config: RocServerConfig,
|
||||
}
|
||||
|
||||
impl std::panic::RefUnwindSafe for RocServer {}
|
||||
|
||||
fn read_env_num(name: &str) -> Option<u64> {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|a| str::parse::<u64>(&a).ok())
|
||||
}
|
||||
|
||||
impl RocServer {
|
||||
pub fn new(client: Client) -> Self {
|
||||
let registry_config = RegistryConfig {
|
||||
latest_document_timeout: Duration::from_millis(
|
||||
read_env_num("ROCLS_LATEST_DOC_TIMEOUT_MS").unwrap_or(5000),
|
||||
),
|
||||
};
|
||||
let config = RocServerConfig {
|
||||
debounce_ms: Duration::from_millis(read_env_num("ROCLS_DEBOUNCE_MS").unwrap_or(100)),
|
||||
};
|
||||
Self {
|
||||
state: RocServerState::new(config, Registry::new(registry_config)),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capabilities() -> ServerCapabilities {
|
||||
@ -61,39 +94,120 @@ impl RocLs {
|
||||
range: None,
|
||||
full: Some(SemanticTokensFullOptions::Bool(true)),
|
||||
});
|
||||
|
||||
let completion_provider = CompletionOptions {
|
||||
resolve_provider: Some(false),
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
all_commit_characters: None,
|
||||
work_done_progress_options: WorkDoneProgressOptions {
|
||||
work_done_progress: None,
|
||||
},
|
||||
};
|
||||
ServerCapabilities {
|
||||
text_document_sync: Some(text_document_sync),
|
||||
hover_provider: Some(hover_provider),
|
||||
definition_provider: Some(OneOf::Right(definition_provider)),
|
||||
document_formatting_provider: Some(OneOf::Right(document_formatting_provider)),
|
||||
semantic_tokens_provider: Some(semantic_tokens_provider),
|
||||
completion_provider: Some(completion_provider),
|
||||
..ServerCapabilities::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a document content change.
|
||||
async fn change(&self, fi: Url, text: String, version: i32) {
|
||||
self.registry()
|
||||
.apply_change(DocumentChange::Modified(fi.clone(), text));
|
||||
let updating_result = self.state.change(&fi, text, version).await;
|
||||
|
||||
let diagnostics = match std::panic::catch_unwind(|| self.registry().diagnostics(&fi)) {
|
||||
Ok(ds) => ds,
|
||||
Err(_) => return,
|
||||
};
|
||||
//The analysis task can be cancelled by another change coming in which will update the watched variable
|
||||
if let Err(e) = updating_result {
|
||||
debug!("Cancelled change. Reason:{:?}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("Applied_changes getting and returning diagnostics");
|
||||
|
||||
let diagnostics = self.state.registry.diagnostics(&fi).await;
|
||||
|
||||
self.client
|
||||
.publish_diagnostics(fi, diagnostics, Some(version))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn close(&self, fi: Url) {
|
||||
self.registry().apply_change(DocumentChange::Closed(fi));
|
||||
impl RocServerState {
|
||||
pub fn new(config: RocServerConfig, registry: Registry) -> RocServerState {
|
||||
Self { config, registry }
|
||||
}
|
||||
|
||||
async fn registry(&self) -> &Registry {
|
||||
&self.registry
|
||||
}
|
||||
|
||||
async fn close(&self, _fi: Url) {}
|
||||
|
||||
pub async fn change(
|
||||
&self,
|
||||
fi: &Url,
|
||||
text: String,
|
||||
version: i32,
|
||||
) -> std::result::Result<(), String> {
|
||||
debug!("V{:?}:starting change", version);
|
||||
let doc_info = DocInfo::new(fi.clone(), text, version);
|
||||
|
||||
self.registry
|
||||
.apply_doc_info_changes(fi.clone(), doc_info.clone())
|
||||
.await;
|
||||
|
||||
debug!(
|
||||
"V{:?}:finished updating docinfo, starting analysis ",
|
||||
version
|
||||
);
|
||||
|
||||
let inner_ref = self;
|
||||
let updating_result = async {
|
||||
//This reduces wasted computation by waiting to allow a new change to come in and update the version before we check, but does delay the final analysis. Ideally this would be replaced with cancelling the analysis when a new one comes in.
|
||||
tokio::time::sleep(self.config.debounce_ms).await;
|
||||
let is_latest = inner_ref
|
||||
.registry
|
||||
.get_latest_version(fi)
|
||||
.await
|
||||
.map(|latest| latest == version)
|
||||
.unwrap_or(true);
|
||||
if !is_latest {
|
||||
return Err("Not latest version skipping analysis".to_string());
|
||||
}
|
||||
|
||||
let results = match tokio::task::spawn_blocking(|| global_analysis(doc_info)).await {
|
||||
Err(e) => return Err(format!("Document analysis failed. reason:{:?}", e)),
|
||||
Ok(a) => a,
|
||||
};
|
||||
let latest_version = inner_ref.registry.get_latest_version(fi).await;
|
||||
|
||||
//if this version is not the latest another change must have come in and this analysis is useless
|
||||
//if there is no older version we can just proceed with the update
|
||||
if let Some(latest_version) = latest_version {
|
||||
if latest_version != version {
|
||||
return Err(format!(
|
||||
"Version {0} doesn't match latest: {1} discarding analysis",
|
||||
version, latest_version
|
||||
));
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"V{:?}:finished document analysis applying changes ",
|
||||
version
|
||||
);
|
||||
|
||||
inner_ref.registry.apply_changes(results, fi.clone()).await;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
debug!("V{:?}:finished document change process", version);
|
||||
updating_result
|
||||
}
|
||||
}
|
||||
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for RocLs {
|
||||
impl LanguageServer for RocServer {
|
||||
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
|
||||
Ok(InitializeResult {
|
||||
capabilities: Self::capabilities(),
|
||||
@ -127,7 +241,7 @@ impl LanguageServer for RocLs {
|
||||
|
||||
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
||||
let TextDocumentIdentifier { uri } = params.text_document;
|
||||
self.close(uri).await;
|
||||
self.state.close(uri).await;
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<()> {
|
||||
@ -144,7 +258,13 @@ impl LanguageServer for RocLs {
|
||||
work_done_progress_params: _,
|
||||
} = params;
|
||||
|
||||
panic_wrapper(|| self.registry().hover(&text_document.uri, position))
|
||||
panic_wrapper_async(|| async {
|
||||
self.state
|
||||
.registry
|
||||
.hover(&text_document.uri, position)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn goto_definition(
|
||||
@ -161,10 +281,14 @@ impl LanguageServer for RocLs {
|
||||
partial_result_params: _,
|
||||
} = params;
|
||||
|
||||
panic_wrapper(|| {
|
||||
self.registry()
|
||||
panic_wrapper_async(|| async {
|
||||
self.state
|
||||
.registry()
|
||||
.await
|
||||
.goto_definition(&text_document.uri, position)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
|
||||
@ -174,7 +298,14 @@ impl LanguageServer for RocLs {
|
||||
work_done_progress_params: _,
|
||||
} = params;
|
||||
|
||||
panic_wrapper(|| self.registry().formatting(&text_document.uri))
|
||||
panic_wrapper_async(|| async {
|
||||
self.state
|
||||
.registry()
|
||||
.await
|
||||
.formatting(&text_document.uri)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn semantic_tokens_full(
|
||||
@ -187,22 +318,184 @@ impl LanguageServer for RocLs {
|
||||
partial_result_params: _,
|
||||
} = params;
|
||||
|
||||
panic_wrapper(|| self.registry().semantic_tokens(&text_document.uri))
|
||||
panic_wrapper_async(|| async {
|
||||
self.state
|
||||
.registry()
|
||||
.await
|
||||
.semantic_tokens(&text_document.uri)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
|
||||
let doc = params.text_document_position;
|
||||
trace!("Got completion request");
|
||||
|
||||
panic_wrapper_async(|| async {
|
||||
self.state
|
||||
.registry
|
||||
.completion_items(&doc.text_document.uri, doc.position)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn panic_wrapper<T>(f: impl FnOnce() -> Option<T> + std::panic::UnwindSafe) -> Result<Option<T>> {
|
||||
async fn panic_wrapper_async<Fut, T>(
|
||||
f: impl FnOnce() -> Fut + std::panic::UnwindSafe,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
Fut: Future<Output = Option<T>>,
|
||||
{
|
||||
match std::panic::catch_unwind(f) {
|
||||
Ok(r) => Ok(r),
|
||||
Ok(r) => Ok(r.await),
|
||||
Err(_) => Err(tower_lsp::jsonrpc::Error::internal_error()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::Builder::from_env("ROCLS_LOG").init();
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdout = tokio::io::stdout();
|
||||
|
||||
let (service, socket) = LspService::new(RocLs::new);
|
||||
let (service, socket) = LspService::new(RocServer::new);
|
||||
Server::new(stdin, stdout, socket).serve(service).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Once;
|
||||
|
||||
use expect_test::expect;
|
||||
use indoc::indoc;
|
||||
use log::info;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn completion_resp_to_labels(resp: CompletionResponse) -> Vec<String> {
|
||||
match resp {
|
||||
CompletionResponse::Array(list) => list.into_iter(),
|
||||
CompletionResponse::List(list) => list.items.into_iter(),
|
||||
}
|
||||
.map(|item| item.label)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
///Gets completion and returns only the label for each completion
|
||||
async fn get_completion_labels(
|
||||
reg: &Registry,
|
||||
url: &Url,
|
||||
position: Position,
|
||||
) -> Option<Vec<String>> {
|
||||
reg.completion_items(url, position)
|
||||
.await
|
||||
.map(completion_resp_to_labels)
|
||||
}
|
||||
|
||||
const DOC_LIT: &str = indoc! {r#"
|
||||
interface Test
|
||||
exposes []
|
||||
imports []
|
||||
"#};
|
||||
static INIT: Once = Once::new();
|
||||
async fn test_setup(doc: String) -> (RocServerState, Url) {
|
||||
INIT.call_once(|| {
|
||||
env_logger::builder()
|
||||
.is_test(true)
|
||||
.filter_level(log::LevelFilter::Trace)
|
||||
.init();
|
||||
});
|
||||
info!("Doc is:\n{0}", doc);
|
||||
let url = Url::parse("file:/Test.roc").unwrap();
|
||||
|
||||
let inner = RocServerState::new(RocServerConfig::default(), Registry::default());
|
||||
//setup the file
|
||||
inner.change(&url, doc, 0).await.unwrap();
|
||||
(inner, url)
|
||||
}
|
||||
///Test that completion works properly when we apply an "as" pattern to an identifier
|
||||
#[tokio::test]
|
||||
async fn test_completion_as_identifier() {
|
||||
let suffix = DOC_LIT.to_string()
|
||||
+ indoc! {r#"
|
||||
main =
|
||||
when a is
|
||||
inn as outer ->
|
||||
"#};
|
||||
let (inner, url) = test_setup(suffix.clone()).await;
|
||||
let position = Position::new(6, 7);
|
||||
let reg = &inner.registry;
|
||||
|
||||
let change = suffix.clone() + "o";
|
||||
inner.change(&url, change, 1).await.unwrap();
|
||||
let comp1 = get_completion_labels(reg, &url, position).await;
|
||||
|
||||
let c = suffix.clone() + "i";
|
||||
inner.change(&url, c, 2).await.unwrap();
|
||||
let comp2 = get_completion_labels(reg, &url, position).await;
|
||||
|
||||
let actual = [comp1, comp2];
|
||||
expect![[r#"
|
||||
[
|
||||
Some(
|
||||
[
|
||||
"outer",
|
||||
],
|
||||
),
|
||||
Some(
|
||||
[
|
||||
"inn",
|
||||
"outer",
|
||||
],
|
||||
),
|
||||
]
|
||||
"#]]
|
||||
.assert_debug_eq(&actual)
|
||||
}
|
||||
|
||||
///Test that completion works properly when we apply an "as" pattern to a record
|
||||
#[tokio::test]
|
||||
async fn test_completion_as_record() {
|
||||
let doc = DOC_LIT.to_string()
|
||||
+ indoc! {r#"
|
||||
main =
|
||||
when a is
|
||||
{one,two} as outer ->
|
||||
"#};
|
||||
|
||||
let (inner, url) = test_setup(doc.clone()).await;
|
||||
let position = Position::new(6, 7);
|
||||
let reg = &inner.registry;
|
||||
|
||||
let change = doc.clone() + "o";
|
||||
inner.change(&url, change, 1).await.unwrap();
|
||||
let comp1 = get_completion_labels(reg, &url, position).await;
|
||||
|
||||
let c = doc.clone() + "t";
|
||||
inner.change(&url, c, 2).await.unwrap();
|
||||
let comp2 = get_completion_labels(reg, &url, position).await;
|
||||
let actual = [comp1, comp2];
|
||||
|
||||
expect![[r#"
|
||||
[
|
||||
Some(
|
||||
[
|
||||
"one",
|
||||
"two",
|
||||
"outer",
|
||||
],
|
||||
),
|
||||
Some(
|
||||
[
|
||||
"one",
|
||||
"two",
|
||||
"outer",
|
||||
],
|
||||
),
|
||||
]
|
||||
"#]]
|
||||
.assert_debug_eq(&actual);
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ pub fn compile_to_mono<'a, 'i, I: Iterator<Item = &'i str>>(
|
||||
target_info: TargetInfo,
|
||||
palette: Palette,
|
||||
) -> (Option<MonomorphizedModule<'a>>, Problems) {
|
||||
let filename = PathBuf::from("");
|
||||
let filename = PathBuf::from("replfile.roc");
|
||||
let src_dir = PathBuf::from("fake/test/path");
|
||||
let (bytes_before_expr, module_src) = promote_expr_to_module(arena, defs, expr);
|
||||
let loaded = roc_load::load_and_monomorphize_from_str(
|
||||
|
@ -4,7 +4,7 @@ use {
|
||||
roc_module::symbol::Interns,
|
||||
roc_mono::{
|
||||
ir::ProcLayout,
|
||||
layout::{GlobalLayoutInterner, LayoutCache, Niche},
|
||||
layout::{GlobalLayoutInterner, LayoutCache, LayoutInterner, Niche},
|
||||
},
|
||||
roc_parse::ast::Expr,
|
||||
roc_repl_eval::{eval::jit_to_ast, ReplAppMemory},
|
||||
@ -57,30 +57,30 @@ pub fn get_values<'a>(
|
||||
|
||||
app.offset = start;
|
||||
|
||||
let expr = {
|
||||
// TODO: pass layout_cache to jit_to_ast directly
|
||||
let mut layout_cache = LayoutCache::new(layout_interner.fork(), target_info);
|
||||
let layout = layout_cache.from_var(arena, variable, subs).unwrap();
|
||||
// TODO: pass layout_cache to jit_to_ast directly
|
||||
let mut layout_cache = LayoutCache::new(layout_interner.fork(), target_info);
|
||||
let layout = layout_cache.from_var(arena, variable, subs).unwrap();
|
||||
|
||||
let proc_layout = ProcLayout {
|
||||
arguments: &[],
|
||||
result: layout,
|
||||
niche: Niche::NONE,
|
||||
};
|
||||
|
||||
jit_to_ast(
|
||||
arena,
|
||||
app,
|
||||
"expect_repl_main_fn",
|
||||
proc_layout,
|
||||
variable,
|
||||
subs,
|
||||
interns,
|
||||
layout_interner.fork(),
|
||||
target_info,
|
||||
)
|
||||
let proc_layout = ProcLayout {
|
||||
arguments: &[],
|
||||
result: layout,
|
||||
niche: Niche::NONE,
|
||||
};
|
||||
|
||||
let expr = jit_to_ast(
|
||||
arena,
|
||||
app,
|
||||
"expect_repl_main_fn",
|
||||
proc_layout,
|
||||
variable,
|
||||
subs,
|
||||
interns,
|
||||
layout_interner.fork(),
|
||||
target_info,
|
||||
);
|
||||
|
||||
app.offset += layout_cache.interner.stack_size_and_alignment(layout).0 as usize;
|
||||
|
||||
result.push(expr);
|
||||
result_vars.push(variable);
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ fn run_expect_pure<'a, W: std::io::Write>(
|
||||
let mut offset = ExpectSequence::START_OFFSET;
|
||||
|
||||
for _ in 0..sequence.count_failures() {
|
||||
offset += render_expect_failure(
|
||||
offset = render_expect_failure(
|
||||
writer,
|
||||
&renderer,
|
||||
arena,
|
||||
@ -731,5 +731,9 @@ pub fn expect_mono_module_to_dylib<'a>(
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(path) = std::env::var("ROC_DEBUG_LLVM") {
|
||||
env.module.print_to_file(path).unwrap();
|
||||
}
|
||||
|
||||
llvm_module_to_dylib(env.module, &target, opt_level).map(|lib| (lib, expects, layout_interner))
|
||||
}
|
||||
|
@ -739,14 +739,14 @@ fn type_problem_unary_operator() {
|
||||
#[test]
|
||||
fn type_problem_string_interpolation() {
|
||||
expect_failure(
|
||||
"\"This is not a string -> \\(1)\"",
|
||||
"\"This is not a string -> $(1)\"",
|
||||
indoc!(
|
||||
r#"
|
||||
── TYPE MISMATCH ───────────────────────────────────────────────────────────────
|
||||
|
||||
This argument to this string interpolation has an unexpected type:
|
||||
|
||||
4│ "This is not a string -> \(1)"
|
||||
4│ "This is not a string -> $(1)"
|
||||
^
|
||||
|
||||
The argument is a number of type:
|
||||
@ -1542,7 +1542,7 @@ fn interpolation_with_nested_interpolation() {
|
||||
expect_failure(
|
||||
indoc!(
|
||||
r#"
|
||||
"foo $(Str.joinWith ["a\(Num.toStr 5)", "b"] "c")"
|
||||
"foo $(Str.joinWith ["a$(Num.toStr 5)", "b"] "c")"
|
||||
"#
|
||||
),
|
||||
indoc!(
|
||||
@ -1551,7 +1551,7 @@ fn interpolation_with_nested_interpolation() {
|
||||
|
||||
This string interpolation is invalid:
|
||||
|
||||
4│ "foo $(Str.joinWith ["a\(Num.toStr 5)", "b"] "c")"
|
||||
4│ "foo $(Str.joinWith ["a$(Num.toStr 5)", "b"] "c")"
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
String interpolations cannot contain newlines or other interpolations.
|
||||
|
@ -3910,6 +3910,28 @@ fn to_packages_report<'a>(
|
||||
severity: Severity::RuntimeError,
|
||||
}
|
||||
}
|
||||
EPackages::ListEnd(pos) => {
|
||||
let surroundings = Region::new(start, pos);
|
||||
let region = LineColumnRegion::from_pos(lines.convert_pos(pos));
|
||||
|
||||
let doc = alloc.stack([
|
||||
alloc.reflow(
|
||||
r"I am partway through parsing a list of packages, but I got stuck here:",
|
||||
),
|
||||
alloc.region_with_subregion(lines.convert_region(surroundings), region),
|
||||
alloc.concat([alloc.reflow("I am expecting a comma or end of list, like")]),
|
||||
alloc
|
||||
.parser_suggestion("packages { package_name: \"url-or-path\", }")
|
||||
.indent(4),
|
||||
]);
|
||||
|
||||
Report {
|
||||
filename,
|
||||
doc,
|
||||
title: "WEIRD PACKAGES LIST".to_string(),
|
||||
severity: Severity::RuntimeError,
|
||||
}
|
||||
}
|
||||
|
||||
EPackages::Space(error, pos) => to_space_report(alloc, lines, filename, &error, pos),
|
||||
|
||||
|
@ -166,7 +166,7 @@ impl<'b> Report<'b> {
|
||||
if self.title.is_empty() {
|
||||
self.doc
|
||||
} else {
|
||||
let header = if self.filename == PathBuf::from("") {
|
||||
let header = if self.filename == PathBuf::from("replfile.roc") {
|
||||
crate::report::pretty_header(&self.title)
|
||||
} else {
|
||||
crate::report::pretty_header_with_path(&self.title, &self.filename)
|
||||
@ -1652,6 +1652,42 @@ pub fn to_file_problem_report<'b>(
|
||||
severity: Severity::Fatal,
|
||||
}
|
||||
}
|
||||
io::ErrorKind::Unsupported => {
|
||||
let doc = match filename.extension() {
|
||||
Some(ext) => alloc.concat(vec![
|
||||
alloc.reflow(r"I expected a file with extension `.roc` or without extension."),
|
||||
alloc.hardline(),
|
||||
alloc.reflow(r"Instead I received a file with extension `."),
|
||||
alloc.as_string(ext.to_string_lossy()),
|
||||
alloc.as_string("`."),
|
||||
]),
|
||||
None => {
|
||||
alloc.stack(vec![
|
||||
alloc.vcat(vec![
|
||||
alloc.reflow(r"I expected a file with either:"),
|
||||
alloc.reflow("- extension `.roc`"),
|
||||
alloc.intersperse(
|
||||
"- no extension and a roc shebang as the first line, e.g. `#!/home/username/bin/roc_nightly/roc`"
|
||||
.split(char::is_whitespace),
|
||||
alloc.concat(vec![ alloc.hardline(), alloc.text(" ")]).flat_alt(alloc.space()).group()
|
||||
),
|
||||
]),
|
||||
alloc.concat(vec![
|
||||
alloc.reflow("The provided file did not start with a shebang `#!` containing the string `roc`. Is "),
|
||||
alloc.as_string(filename.to_string_lossy()),
|
||||
alloc.reflow(" a Roc file?"),
|
||||
])
|
||||
])
|
||||
}
|
||||
};
|
||||
|
||||
Report {
|
||||
filename,
|
||||
doc,
|
||||
title: "NOT A ROC FILE".to_string(),
|
||||
severity: Severity::Fatal,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let error = std::io::Error::from(error);
|
||||
let formatted = format!("{error}");
|
||||
|
@ -310,10 +310,9 @@ impl RocDec {
|
||||
}
|
||||
};
|
||||
|
||||
let opt_after_point = match parts.next() {
|
||||
Some(answer) if answer.len() <= Self::DECIMAL_PLACES => Some(answer),
|
||||
_ => None,
|
||||
};
|
||||
let opt_after_point = parts
|
||||
.next()
|
||||
.map(|answer| &answer[..Ord::min(answer.len(), Self::DECIMAL_PLACES)]);
|
||||
|
||||
// There should have only been one "." in the string!
|
||||
if parts.next().is_some() {
|
||||
|
@ -300,6 +300,11 @@ mod test_roc_std {
|
||||
|
||||
let example = RocDec::from_str("1000.000").unwrap();
|
||||
assert_eq!(format!("{example}"), "1000");
|
||||
|
||||
// truncate if there are more digits than supported
|
||||
let example =
|
||||
RocDec::from_str("3.14159265358979323846264338327950288419716939937510").unwrap();
|
||||
assert_eq!(format!("{example}"), "3.141592653589793238");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -345,7 +345,7 @@ fn joinpoint_with_closure() {
|
||||
catSound = makeSound Cat
|
||||
dogSound = makeSound Dog
|
||||
gooseSound = makeSound Goose
|
||||
"Cat: \(catSound), Dog: \(dogSound), Goose: \(gooseSound)"
|
||||
"Cat: $(catSound), Dog: $(dogSound), Goose: $(gooseSound)"
|
||||
|
||||
test
|
||||
)
|
||||
@ -374,7 +374,7 @@ fn joinpoint_with_reuse() {
|
||||
Cons x xs ->
|
||||
strX = f x
|
||||
strXs = printLinkedList xs f
|
||||
"Cons \(strX) (\(strXs))"
|
||||
"Cons $(strX) ($(strXs))"
|
||||
|
||||
test =
|
||||
newList = mapLinkedList (Cons 1 (Cons 2 (Cons 3 Nil))) (\x -> x + 1)
|
||||
@ -461,7 +461,7 @@ fn tree_rebalance() {
|
||||
sL = nodeInParens left showKey showValue
|
||||
sR = nodeInParens right showKey showValue
|
||||
|
||||
"Node \(sColor) \(sKey) \(sValue) \(sL) \(sR)"
|
||||
"Node $(sColor) $(sKey) $(sValue) $(sL) $(sR)"
|
||||
|
||||
nodeInParens : RedBlackTree k v, (k -> Str), (v -> Str) -> Str
|
||||
nodeInParens = \tree, showKey, showValue ->
|
||||
@ -472,7 +472,7 @@ fn tree_rebalance() {
|
||||
Node _ _ _ _ _ ->
|
||||
inner = showRBTree tree showKey showValue
|
||||
|
||||
"(\(inner))"
|
||||
"($(inner))"
|
||||
|
||||
showColor : NodeColor -> Str
|
||||
showColor = \color ->
|
||||
@ -521,7 +521,7 @@ fn joinpoint_nullpointer() {
|
||||
Nil -> "Nil"
|
||||
Cons x xs ->
|
||||
strXs = printLinkedList xs
|
||||
"Cons \(x) (\(strXs))"
|
||||
"Cons $(x) ($(strXs))"
|
||||
|
||||
linkedListHead : LinkedList Str -> LinkedList Str
|
||||
linkedListHead = \linkedList ->
|
||||
@ -533,7 +533,7 @@ fn joinpoint_nullpointer() {
|
||||
test =
|
||||
cons = printLinkedList (linkedListHead (Cons "foo" Nil))
|
||||
nil = printLinkedList (linkedListHead (Nil))
|
||||
"\(cons) - \(nil)"
|
||||
"$(cons) - $(nil)"
|
||||
|
||||
test
|
||||
)
|
||||
|
@ -1,5 +1,7 @@
|
||||
# devtools
|
||||
|
||||
[Install nix](https://github.com/roc-lang/roc/blob/main/BUILDING_FROM_SOURCE.md#installing-nix) if you have not already done so.
|
||||
|
||||
To make rust-analyzer and other vscode extensions work well you want them using the same rustc, glibc, zig... as specified in the roc nix flake.
|
||||
The easiest way to do this is to use another flake for all your dev tools that takes the roc flake as an input.
|
||||
|
||||
|
@ -148,7 +148,7 @@ bool = \b ->
|
||||
str : Str -> Inspector GuiFormatter
|
||||
str = \s ->
|
||||
f0 <- Inspect.custom
|
||||
addNode f0 (Text "\"\(s)\"")
|
||||
addNode f0 (Text "\"$(s)\"")
|
||||
|
||||
opaque : * -> Inspector GuiFormatter
|
||||
opaque = \_ ->
|
||||
|
@ -13,6 +13,6 @@ tick = \n ->
|
||||
_ <- await (Stdout.line "🎉 SURPRISE! Happy Birthday! 🎂")
|
||||
Task.ok (Done {})
|
||||
else
|
||||
_ <- await (n |> Num.toStr |> \s -> "\(s)..." |> Stdout.line)
|
||||
_ <- await (n |> Num.toStr |> \s -> "$(s)..." |> Stdout.line)
|
||||
_ <- await Stdin.line
|
||||
Task.ok (Step (n - 1))
|
||||
|
@ -9,7 +9,7 @@ main =
|
||||
(Effect.getLine)
|
||||
\line ->
|
||||
Effect.after
|
||||
(Effect.putLine "You entered: \(line)")
|
||||
(Effect.putLine "You entered: $(line)")
|
||||
\{} ->
|
||||
Effect.after
|
||||
(Effect.putLine "It is known")
|
||||
|
@ -7,7 +7,7 @@ main : Task {} I32
|
||||
main =
|
||||
task =
|
||||
Env.decode "EDITOR"
|
||||
|> Task.await (\editor -> Stdout.line "Your favorite editor is \(editor)!")
|
||||
|> Task.await (\editor -> Stdout.line "Your favorite editor is $(editor)!")
|
||||
|> Task.await (\{} -> Env.decode "SHLVL")
|
||||
|> Task.await
|
||||
(\lvl ->
|
||||
@ -16,7 +16,7 @@ main =
|
||||
n ->
|
||||
lvlStr = Num.toStr n
|
||||
|
||||
Stdout.line "Your current shell level is \(lvlStr)!")
|
||||
Stdout.line "Your current shell level is $(lvlStr)!")
|
||||
|> Task.await \{} -> Env.decode "LETTERS"
|
||||
|
||||
Task.attempt task \result ->
|
||||
@ -24,7 +24,7 @@ main =
|
||||
Ok letters ->
|
||||
joinedLetters = Str.joinWith letters " "
|
||||
|
||||
Stdout.line "Your favorite letters are: \(joinedLetters)"
|
||||
Stdout.line "Your favorite letters are: $(joinedLetters)"
|
||||
|
||||
Err _ ->
|
||||
Stderr.line "I couldn't find your favorite letters in the environment variables!"
|
||||
|
@ -55,7 +55,7 @@ toStr = \{ scopes, stack, state, vars } ->
|
||||
stackStr = Str.joinWith (List.map stack toStrData) " "
|
||||
varsStr = Str.joinWith (List.map vars toStrData) " "
|
||||
|
||||
"\n============\nDepth: \(depth)\nState: \(stateStr)\nStack: [\(stackStr)]\nVars: [\(varsStr)]\n============\n"
|
||||
"\n============\nDepth: $(depth)\nState: $(stateStr)\nStack: [$(stackStr)]\nVars: [$(varsStr)]\n============\n"
|
||||
|
||||
with : Str, (Context -> Task {} a) -> Task {} a
|
||||
with = \path, callback ->
|
||||
|
@ -21,7 +21,7 @@ InterpreterErrors : [BadUtf8, DivByZero, EmptyStack, InvalidBooleanValue, Invali
|
||||
main : Str -> Task {} []
|
||||
main = \filename ->
|
||||
interpretFile filename
|
||||
|> Task.onFail \StringErr e -> Stdout.line "Ran into problem:\n\(e)\n"
|
||||
|> Task.onFail \StringErr e -> Stdout.line "Ran into problem:\n$(e)\n"
|
||||
|
||||
interpretFile : Str -> Task {} [StringErr Str]
|
||||
interpretFile = \filename ->
|
||||
@ -44,7 +44,7 @@ interpretFile = \filename ->
|
||||
Task.fail (StringErr "Ran into an invalid boolean that was neither false (0) or true (-1)")
|
||||
|
||||
Err (InvalidChar char) ->
|
||||
Task.fail (StringErr "Ran into an invalid character with ascii code: \(char)")
|
||||
Task.fail (StringErr "Ran into an invalid character with ascii code: $(char)")
|
||||
|
||||
Err MaxInputNumber ->
|
||||
Task.fail (StringErr "Like the original false compiler, the max input number is 320,000")
|
||||
|
@ -18,15 +18,15 @@ main =
|
||||
cwd <- Env.cwd |> Task.await
|
||||
cwdStr = Path.display cwd
|
||||
|
||||
_ <- Stdout.line "cwd: \(cwdStr)" |> Task.await
|
||||
_ <- Stdout.line "cwd: $(cwdStr)" |> Task.await
|
||||
dirEntries <- Dir.list cwd |> Task.await
|
||||
contentsStr = Str.joinWith (List.map dirEntries Path.display) "\n "
|
||||
|
||||
_ <- Stdout.line "Directory contents:\n \(contentsStr)\n" |> Task.await
|
||||
_ <- Stdout.line "Directory contents:\n $(contentsStr)\n" |> Task.await
|
||||
_ <- Stdout.line "Writing a string to out.txt" |> Task.await
|
||||
_ <- File.writeUtf8 path "a string!" |> Task.await
|
||||
contents <- File.readUtf8 path |> Task.await
|
||||
Stdout.line "I read the file back. Its contents: \"\(contents)\""
|
||||
Stdout.line "I read the file back. Its contents: \"$(contents)\""
|
||||
|
||||
Task.attempt task \result ->
|
||||
when result is
|
||||
|
@ -11,7 +11,7 @@ main =
|
||||
_ <- await (Stdout.line "What's your last name?")
|
||||
lastName <- await Stdin.line
|
||||
|
||||
Stdout.line "Hi, \(unwrap firstName) \(unwrap lastName)! 👋"
|
||||
Stdout.line "Hi, $(unwrap firstName) $(unwrap lastName)! 👋"
|
||||
|
||||
unwrap : [Input Str, End] -> Str
|
||||
unwrap = \input ->
|
||||
|
@ -39,7 +39,7 @@ Just so you know what to expect, our Roc functions look like this;
|
||||
``` coffee
|
||||
interpolateString : Str -> Str
|
||||
interpolateString = \name ->
|
||||
"Hello from Roc \(name)!!!🤘🤘🤘"
|
||||
"Hello from Roc $(name)!!!🤘🤘🤘"
|
||||
|
||||
|
||||
mulArrByScalar : List I32, I32 -> List I32
|
||||
|
@ -5,7 +5,7 @@ app "rocdemo"
|
||||
|
||||
interpolateString : Str -> Str
|
||||
interpolateString = \name ->
|
||||
"Hello from Roc \(name)!!!🤘🤘🤘"
|
||||
"Hello from Roc $(name)!!!🤘🤘🤘"
|
||||
|
||||
# jint is i32
|
||||
mulArrByScalar : List I32, I32 -> List I32
|
||||
|
@ -5,4 +5,4 @@ app "libhello"
|
||||
|
||||
main : Str -> Str
|
||||
main = \message ->
|
||||
"TypeScript said to Roc: \(message)! 🎉"
|
||||
"TypeScript said to Roc: $(message)! 🎉"
|
||||
|
@ -21,7 +21,7 @@ main =
|
||||
|> List.map \_ -> 1
|
||||
|> List.sum
|
||||
|> Num.toStr
|
||||
|> \countLetterA -> Stdout.line "I counted \(countLetterA) letter A's!"
|
||||
|> \countLetterA -> Stdout.line "I counted $(countLetterA) letter A's!"
|
||||
|
||||
Err _ -> Stderr.line "Ooops, something went wrong parsing letters"
|
||||
|
||||
|
@ -26,20 +26,20 @@ main =
|
||||
|> Str.joinWith ("\n")
|
||||
nMovies = List.len movies |> Num.toStr
|
||||
|
||||
Stdout.line "\(nMovies) movies were found:\n\n\(moviesString)\n\nParse success!\n"
|
||||
Stdout.line "$(nMovies) movies were found:\n\n$(moviesString)\n\nParse success!\n"
|
||||
|
||||
Err problem ->
|
||||
when problem is
|
||||
ParsingFailure failure ->
|
||||
Stderr.line "Parsing failure: \(failure)\n"
|
||||
Stderr.line "Parsing failure: $(failure)\n"
|
||||
|
||||
ParsingIncomplete leftover ->
|
||||
leftoverStr = leftover |> List.map strFromUtf8 |> List.map (\val -> "\"\(val)\"") |> Str.joinWith ", "
|
||||
leftoverStr = leftover |> List.map strFromUtf8 |> List.map (\val -> "\"$(val)\"") |> Str.joinWith ", "
|
||||
|
||||
Stderr.line "Parsing incomplete. Following leftover fields while parsing a record: \(leftoverStr)\n"
|
||||
Stderr.line "Parsing incomplete. Following leftover fields while parsing a record: $(leftoverStr)\n"
|
||||
|
||||
SyntaxError error ->
|
||||
Stderr.line "Parsing failure. Syntax error in the CSV: \(error)"
|
||||
Stderr.line "Parsing failure. Syntax error in the CSV: $(error)"
|
||||
|
||||
MovieInfo := { title : Str, releaseYear : U64, actors : List Str }
|
||||
|
||||
@ -57,7 +57,7 @@ movieInfoExplanation = \@MovieInfo { title, releaseYear, actors } ->
|
||||
enumeratedActors = enumerate actors
|
||||
releaseYearStr = Num.toStr releaseYear
|
||||
|
||||
"The movie '\(title)' was released in \(releaseYearStr) and stars \(enumeratedActors)"
|
||||
"The movie '$(title)' was released in $(releaseYearStr) and stars $(enumeratedActors)"
|
||||
|
||||
enumerate : List Str -> Str
|
||||
enumerate = \elements ->
|
||||
|
@ -10,4 +10,4 @@ main = \num ->
|
||||
else
|
||||
str = Num.toStr num
|
||||
|
||||
"The number was \(str), OH YEAH!!! 🤘🤘"
|
||||
"The number was $(str), OH YEAH!!! 🤘🤘"
|
||||
|
@ -10,4 +10,4 @@ main = \num ->
|
||||
else
|
||||
str = Num.toStr num
|
||||
|
||||
"The number was \(str), OH YEAH!!! 🤘🤘"
|
||||
"The number was $(str), OH YEAH!!! 🤘🤘"
|
||||
|
@ -54,12 +54,21 @@ Herculei undae calcata inmeriti quercus ignes parabant iam.
|
||||
|
||||
### Example Code Blocks
|
||||
|
||||
#### Something That Cannot Be Found
|
||||
|
||||
```sh
|
||||
# This isn't fenced roc code so its not formatted
|
||||
# Use a fence like ```roc to format code blocks
|
||||
```
|
||||
|
||||
#### A Complete File
|
||||
|
||||
```roc
|
||||
file:codeExample.roc
|
||||
```
|
||||
|
||||
#### A Snippet Of Some File
|
||||
|
||||
```roc
|
||||
file:codeExample.roc:snippet:view
|
||||
```
|
||||
|
@ -29,6 +29,7 @@ transformFileContent = \currentUrl, htmlContent ->
|
||||
|> Result.map Html.render
|
||||
|> Result.withDefault ""
|
||||
|
||||
### start snippet view
|
||||
view : NavLink, Str -> Html.Node
|
||||
view = \currentNavLink, htmlContent ->
|
||||
html [lang "en"] [
|
||||
@ -37,6 +38,7 @@ view = \currentNavLink, htmlContent ->
|
||||
Html.title [] [text currentNavLink.title],
|
||||
link [rel "stylesheet", href "style.css"] [],
|
||||
],
|
||||
### start snippet body
|
||||
body [] [
|
||||
div [class "main"] [
|
||||
div [class "navbar"] [
|
||||
@ -49,7 +51,9 @@ view = \currentNavLink, htmlContent ->
|
||||
],
|
||||
],
|
||||
],
|
||||
### end snippet body
|
||||
]
|
||||
### end snippet view
|
||||
|
||||
viewNavbar : NavLink -> Html.Node
|
||||
viewNavbar = \currentNavLink ->
|
||||
|
@ -208,29 +208,29 @@ renderHelp = \buffer, node ->
|
||||
Str.concat buffer content
|
||||
|
||||
Element tagName _ attrs children ->
|
||||
withTagName = "\(buffer)<\(tagName)"
|
||||
withTagName = "$(buffer)<$(tagName)"
|
||||
withAttrs =
|
||||
if List.isEmpty attrs then
|
||||
withTagName
|
||||
else
|
||||
List.walk attrs "\(withTagName) " renderAttr
|
||||
List.walk attrs "$(withTagName) " renderAttr
|
||||
withTag = Str.concat withAttrs ">"
|
||||
withChildren = List.walk children withTag renderHelp
|
||||
|
||||
"\(withChildren)</\(tagName)>"
|
||||
"$(withChildren)</$(tagName)>"
|
||||
|
||||
UnclosedElem tagName _ attrs ->
|
||||
if List.isEmpty attrs then
|
||||
"\(buffer)<\(tagName)>"
|
||||
"$(buffer)<$(tagName)>"
|
||||
else
|
||||
attrs
|
||||
|> List.walk "\(buffer)<\(tagName) " renderAttr
|
||||
|> List.walk "$(buffer)<$(tagName) " renderAttr
|
||||
|> Str.concat ">"
|
||||
|
||||
# internal helper
|
||||
renderAttr : Str, Attribute -> Str
|
||||
renderAttr = \buffer, Attribute key val ->
|
||||
"\(buffer) \(key)=\"\(val)\""
|
||||
"$(buffer) $(key)=\"$(val)\""
|
||||
|
||||
# Main root
|
||||
html = element "html"
|
||||
|
@ -184,7 +184,7 @@ coords = attribute "coords"
|
||||
crossorigin = attribute "crossorigin"
|
||||
csp = attribute "csp"
|
||||
data = attribute "data"
|
||||
dataAttr = \dataName, dataVal -> Attribute "data-\(dataName)" dataVal
|
||||
dataAttr = \dataName, dataVal -> Attribute "data-$(dataName)" dataVal
|
||||
datetime = attribute "datetime"
|
||||
decoding = attribute "decoding"
|
||||
default = attribute "default"
|
||||
|
@ -3,13 +3,11 @@ use libc;
|
||||
use pulldown_cmark::{html, Options, Parser};
|
||||
use roc_std::{RocBox, RocStr};
|
||||
use std::env;
|
||||
use std::ffi::CStr;
|
||||
use std::fs;
|
||||
use std::os::raw::c_char;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::{Style, ThemeSet};
|
||||
use syntect::highlighting::{ThemeSet};
|
||||
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::util::LinesWithEndings;
|
||||
@ -244,17 +242,19 @@ fn process_file(input_dir: &Path, output_dir: &Path, input_file: &Path) -> Resul
|
||||
pulldown_cmark::CodeBlockKind::Fenced(extention_str),
|
||||
)) => {
|
||||
if in_code_block {
|
||||
match replace_code_with_static_file(&code_to_highlight, input_file) {
|
||||
None => {}
|
||||
// Check if the code block is actually just a relative
|
||||
// path to a static file, if so replace the code with
|
||||
// the contents of the file.
|
||||
// ```
|
||||
// file:myCodeFile.roc
|
||||
// ```
|
||||
Some(new_code_to_highlight) => {
|
||||
code_to_highlight = new_code_to_highlight;
|
||||
match &code_to_highlight.split(':').collect::<Vec<_>>()[..] {
|
||||
["file", replacement_file_name, "snippet", snippet_name] => {
|
||||
code_to_highlight = read_replacement_snippet(
|
||||
replacement_file_name.trim(),
|
||||
snippet_name.trim(),
|
||||
input_file,
|
||||
);
|
||||
}
|
||||
["file", replacement_file_name] => {
|
||||
code_to_highlight =
|
||||
read_replacement_file(replacement_file_name.trim(), input_file);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Format the whole multi-line code block as HTML all at once
|
||||
@ -355,45 +355,74 @@ pub fn strip_windows_prefix(path_buf: PathBuf) -> std::path::PathBuf {
|
||||
fn is_roc_code_block(cbk: &pulldown_cmark::CodeBlockKind) -> bool {
|
||||
match cbk {
|
||||
pulldown_cmark::CodeBlockKind::Indented => false,
|
||||
pulldown_cmark::CodeBlockKind::Fenced(cow_str) => {
|
||||
if cow_str.contains("roc") {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
pulldown_cmark::CodeBlockKind::Fenced(cow_str) => cow_str.contains("roc")
|
||||
}
|
||||
}
|
||||
|
||||
fn read_replacement_file(replacement_file_name: &str, input_file: &Path) -> String {
|
||||
if replacement_file_name.contains("../") {
|
||||
panic!(
|
||||
"ERROR File \"{}\" must be located within the input diretory.",
|
||||
replacement_file_name
|
||||
);
|
||||
}
|
||||
|
||||
let input_dir = input_file.parent().unwrap();
|
||||
let replacement_file_path = input_dir.join(replacement_file_name);
|
||||
|
||||
match fs::read(&replacement_file_path) {
|
||||
Ok(content) => String::from_utf8(content).unwrap(),
|
||||
Err(err) => {
|
||||
panic!(
|
||||
"ERROR File \"{}\" is unreadable:\n\t{}",
|
||||
replacement_file_path.to_str().unwrap(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_code_with_static_file(code: &str, input_file: &Path) -> Option<String> {
|
||||
let input_dir = input_file.parent()?;
|
||||
let trimmed_code = code.trim();
|
||||
fn remove_snippet_comments(input: &str) -> String {
|
||||
let line_ending = if input.contains("\r\n") { "\r\n" } else { "\n" };
|
||||
|
||||
// Confirm the code block starts with a `file:` tag
|
||||
match trimmed_code.strip_prefix("file:") {
|
||||
None => None,
|
||||
Some(path) => {
|
||||
// File must be located in input folder or sub-directory
|
||||
if path.contains("../") {
|
||||
panic!("ERROR File must be located within the input diretory!");
|
||||
}
|
||||
input
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
!line.contains("### start snippet") && !line.contains("### end snippet")
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join(line_ending)
|
||||
}
|
||||
|
||||
let file_path = input_dir.join(path);
|
||||
fn read_replacement_snippet(
|
||||
replacement_file_name: &str,
|
||||
snippet_name: &str,
|
||||
input_file: &Path,
|
||||
) -> String {
|
||||
let start_marker = format!("### start snippet {}", snippet_name);
|
||||
let end_marker = format!("### end snippet {}", snippet_name);
|
||||
|
||||
// Check file exists before opening
|
||||
match file_path.try_exists() {
|
||||
Err(_) | Ok(false) => {
|
||||
panic!(
|
||||
"ERROR File does not exist: \"{}\"",
|
||||
file_path.to_str().unwrap()
|
||||
);
|
||||
}
|
||||
Ok(true) => {
|
||||
let vec_u8 = fs::read(file_path).ok()?;
|
||||
let replacement_file_content = read_replacement_file(replacement_file_name.trim(), input_file);
|
||||
|
||||
String::from_utf8(vec_u8).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
let start_position = replacement_file_content
|
||||
.find(&start_marker)
|
||||
.expect(format!("ERROR Failed to find snippet start \"{}\". ", &start_marker).as_str());
|
||||
|
||||
let end_position = replacement_file_content
|
||||
.find(&end_marker)
|
||||
.expect(format!("ERROR Failed to find snippet end \"{}\". ", &end_marker).as_str());
|
||||
|
||||
if start_position >= end_position {
|
||||
let start_position_str = start_position.to_string();
|
||||
let end_position_str = end_position.to_string();
|
||||
|
||||
panic!(
|
||||
"ERROR Detected start position ({start_position_str}) of snippet \"{snippet_name}\" was greater than or equal to detected end position ({end_position_str})."
|
||||
);
|
||||
} else {
|
||||
// We want to remove other snippet comments inside this one if they exist.
|
||||
remove_snippet_comments(
|
||||
&replacement_file_content[start_position + start_marker.len()..end_position]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ render = \state ->
|
||||
head [] [],
|
||||
body [] [
|
||||
h1 [] [text "The app"],
|
||||
div [] [text "The answer is \(num)"],
|
||||
div [] [text "The answer is $(num)"],
|
||||
],
|
||||
]
|
||||
|
||||
|
@ -762,7 +762,7 @@ expect
|
||||
# Sizes don't matter, use zero. We are not creating a HTML string so we don't care what size it would be.
|
||||
Element "body" 0 [] [
|
||||
Element "h1" 0 [] [Text "The app"],
|
||||
Element "div" 0 [onClickAttr] [Text "The answer is \(num)"],
|
||||
Element "div" 0 [onClickAttr] [Text "The answer is $(num)"],
|
||||
]
|
||||
|
||||
app : App State State
|
||||
|
@ -38,7 +38,7 @@ logRequest : Request -> Task {} AppError
|
||||
logRequest = \req ->
|
||||
dateTime <- Utc.now |> Task.map Utc.toIso8601Str |> Task.await
|
||||
|
||||
Stdout.line "\(dateTime) \(Http.methodToStr req.method) \(req.url)"
|
||||
Stdout.line "$(dateTime) $(Http.methodToStr req.method) $(req.url)"
|
||||
|
||||
readUrlEnv : Str -> Task Str AppError
|
||||
readUrlEnv = \target ->
|
||||
@ -60,7 +60,7 @@ handleErr = \err ->
|
||||
HttpError _ -> "Http error fetching content"
|
||||
|
||||
# Log error to stderr
|
||||
{} <- Stderr.line "Internal Server Error: \(message)" |> Task.await
|
||||
{} <- Stderr.line "Internal Server Error: $(message)" |> Task.await
|
||||
_ <- Stderr.flush |> Task.attempt
|
||||
|
||||
# Respond with Http 500 Error
|
||||
|
@ -13,7 +13,7 @@ main = \req ->
|
||||
|
||||
# Log request date, method and url
|
||||
date <- Utc.now |> Task.map Utc.toIso8601Str |> Task.await
|
||||
{} <- Stdout.line "\(date) \(Http.methodToStr req.method) \(req.url)" |> Task.await
|
||||
{} <- Stdout.line "$(date) $(Http.methodToStr req.method) $(req.url)" |> Task.await
|
||||
|
||||
# Respond with request body
|
||||
when req.body is
|
||||
|
@ -36,7 +36,7 @@ view =
|
||||
Newline,
|
||||
Desc [Ident "user", Kw "<-", Ident "Http.get", Ident "url", Ident "Json.codec", Kw "|>", Ident "Task.await"] "<p>This fetches the contents of the URL and decodes them as <a href=\"https://www.json.org\">JSON</a>.</p><p>If the shape of the JSON isn't compatible with the type of <code>user</code> (based on type inference), this will give a decoding error immediately.</p><p>As with all the other lines ending in <code>|> Task.await</code>, if there's an error, nothing else in <code>storeEmail</code> will be run, and <code>handleErr</code> will end up handling the error.</p>",
|
||||
Newline,
|
||||
Desc [Ident "dest", Kw "=", Ident "Path.fromStr", StrInterpolation "\"" "user.name" ".txt\""] "<p>The <code>\\(user.name)</code> in this string literal will be replaced with the value in <code>name</code>. This is <a href=\"/tutorial#string-interpolation\">string interpolation</a>.</p><p>Note that this line doesn't end with <code>|> Task.await</code>. Earlier lines needed that because they were I/O <a href=\"/tutorial#tasks\">tasks</a>, but this is a plain old <a href=\"/tutorial#defs\">definition</a>, so there's no task to await.</p>",
|
||||
Desc [Ident "dest", Kw "=", Ident "Path.fromStr", StrInterpolation "\"" "user.name" ".txt\""] "<p>The <code>\$(user.name)</code> in this string literal will be replaced with the value in <code>name</code>. This is <a href=\"/tutorial#string-interpolation\">string interpolation</a>.</p><p>Note that this line doesn't end with <code>|> Task.await</code>. Earlier lines needed that because they were I/O <a href=\"/tutorial#tasks\">tasks</a>, but this is a plain old <a href=\"/tutorial#defs\">definition</a>, so there's no task to await.</p>",
|
||||
Newline,
|
||||
Desc [Literal "_", Kw "<-", Ident "File.writeUtf8", Ident "dest", Ident "user.email", Kw "|>", Ident "Task.await"] "<p>This writes <code>user.email</code> to the file, encoded as <a href=\"https://en.wikipedia.org/wiki/UTF-8\">UTF-8</a>.</p><p>We won't be using the output of <code>writeUtf8</code>, so we name it <code>_</code>. The special name <code>_</code> is for when you don't want to use something.</p><p>You can name as many things as you like <code>_</code>, but you can never reference anything named <code>_</code>. So it's only useful for when you don't want to choose a name.</p>",
|
||||
Newline,
|
||||
@ -89,7 +89,7 @@ tokensToStr = \tokens ->
|
||||
# Don't put spaces after opening parens or before closing parens
|
||||
argsWithCommas =
|
||||
args
|
||||
|> List.map \ident -> "<span class=\"ident\">\(ident)</span>"
|
||||
|> List.map \ident -> "<span class=\"ident\">$(ident)</span>"
|
||||
|> Str.joinWith "<span class=\"literal\">,</span> "
|
||||
|
||||
bufWithSpace
|
||||
@ -98,32 +98,32 @@ tokensToStr = \tokens ->
|
||||
|> Str.concat "<span class=\"kw\"> -></span>"
|
||||
|
||||
Kw str ->
|
||||
Str.concat bufWithSpace "<span class=\"kw\">\(str)</span>"
|
||||
Str.concat bufWithSpace "<span class=\"kw\">$(str)</span>"
|
||||
|
||||
Num str | Str str | Literal str -> # We may render these differently in the future
|
||||
Str.concat bufWithSpace "<span class=\"literal\">\(str)</span>"
|
||||
Str.concat bufWithSpace "<span class=\"literal\">$(str)</span>"
|
||||
|
||||
Comment str ->
|
||||
Str.concat bufWithSpace "<span class=\"comment\"># \(str)</span>"
|
||||
Str.concat bufWithSpace "<span class=\"comment\"># $(str)</span>"
|
||||
|
||||
Ident str ->
|
||||
Str.concat bufWithSpace (identToHtml str)
|
||||
|
||||
StrInterpolation before interp after ->
|
||||
bufWithSpace
|
||||
|> Str.concat (if Str.isEmpty before then "" else "<span class=\"literal\">\(before)</span>")
|
||||
|> Str.concat "<span class=\"kw\">\\(</span>\(identToHtml interp)<span class=\"kw\">)</span>"
|
||||
|> Str.concat (if Str.isEmpty after then "" else "<span class=\"literal\">\(after)</span>")
|
||||
|> Str.concat (if Str.isEmpty before then "" else "<span class=\"literal\">$(before)</span>")
|
||||
|> Str.concat "<span class=\"kw\">\$(</span>$(identToHtml interp)<span class=\"kw\">)</span>"
|
||||
|> Str.concat (if Str.isEmpty after then "" else "<span class=\"literal\">$(after)</span>")
|
||||
|
||||
identToHtml : Str -> Str
|
||||
identToHtml = \str ->
|
||||
List.walk (Str.split str ".") "" \accum, ident ->
|
||||
identHtml = "<span class=\"ident\">\(ident)</span>"
|
||||
identHtml = "<span class=\"ident\">$(ident)</span>"
|
||||
|
||||
if Str.isEmpty accum then
|
||||
identHtml
|
||||
else
|
||||
"\(accum)<span class=\"kw\">.</span>\(identHtml)"
|
||||
"$(accum)<span class=\"kw\">.</span>$(identHtml)"
|
||||
|
||||
sectionsToStr : List Section -> Str
|
||||
sectionsToStr = \sections ->
|
||||
@ -168,5 +168,5 @@ radio = \index, labelHtml, descHtml ->
|
||||
checkedHtml = if index == 0 then " checked" else ""
|
||||
|
||||
"""
|
||||
<input class="interactive-radio" type="radio" name="r" id="r\(Num.toStr index)" \(checkedHtml)><label for="r\(Num.toStr index)" title="Tap to learn about this syntax">\(labelHtml)</label><span class="interactive-desc" role="presentation"><button class="close-desc">X</button>\(descHtml)</span>
|
||||
<input class="interactive-radio" type="radio" name="r" id="r$(Num.toStr index)" $(checkedHtml)><label for="r$(Num.toStr index)" title="Tap to learn about this syntax">$(labelHtml)</label><span class="interactive-desc" role="presentation"><button class="close-desc">X</button>$(descHtml)</span>
|
||||
"""
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
- Linux or MacOS operating system, Windows users can use linux through WSL.
|
||||
- Install [git](https://chat.openai.com/share/71fb3ae6-80d7-478c-8a27-a36aaa5ba921)
|
||||
- Install [nix](https://nixos.org/download.html)
|
||||
- Install [nix](https://github.com/roc-lang/roc/blob/main/BUILDING_FROM_SOURCE.md#installing-nix)
|
||||
|
||||
## Building the website from scratch
|
||||
|
||||
|
@ -9,7 +9,7 @@ Roc's syntax isn't trivial, but there also isn't much of it to learn. It's desig
|
||||
- `user.email` always accesses the `email` field of a record named `user`. <span class="nowrap">(Roc has</span> no inheritance, subclassing, or proxying.)
|
||||
- `Email.isValid` always refers to something named `isValid` exported by a module named `Email`. (Module names are always capitalized, and variables/constants never are.) Modules are always defined statically and can't be modified at runtime; there's no [monkey patching](https://en.wikipedia.org/wiki/Monkey_patch) to consider either.
|
||||
- `x = doSomething y z` always declares a new constant `x` (Roc has [no mutable variables, reassignment, or shadowing](/functional)) to be whatever the `doSomething` function returns when passed the arguments `y` and `z`. (Function calls in Roc don't need parentheses or commas.)
|
||||
- `"Name: \(Str.trim name)"` uses *string interpolation* syntax: a backslash inside a string literal, followed by an expression in parentheses. This code is the same as combining the string `"Name: "` with the string returned by the function call `Str.trim name`.<br><br>Because Roc's string interpolation syntax begins with a backslash (just like other backlash-escapes such as `\n` and `\"`), you can always tell which parts of a string involve special handling: the parts that begin with backslashes. Everything else works as normal.
|
||||
- `"Name: $(Str.trim name)"` uses *string interpolation* syntax: a dollar sign inside a string literal, followed by an expression in parentheses.
|
||||
|
||||
Roc also ships with a source code formatter that helps you maintain a consistent style with little effort. The `roc format` command neatly formats your source code according to a common style, and it's designed with the time-saving feature of having no configuration options. This feature saves teams all the time they would otherwise spend debating which stylistic tweaks to settle on!
|
||||
|
||||
|
@ -65,7 +65,7 @@ A benefit of this design is that it makes Roc code easier to rearrange without c
|
||||
|
||||
<pre><samp class="code-snippet">func <span class="kw">=</span> <span class="kw">\</span>arg <span class="kw">-></span>
|
||||
greeting <span class="kw">=</span> <span class="string">"Hello"</span>
|
||||
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-></span> <span class="string">"</span><span class="kw">\(</span>greeting<span class="kw">)</span><span class="string">, </span><span class="kw">\(</span>name<span class="kw">)</span><span class="string">!"</span>
|
||||
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-></span> <span class="string">"</span><span class="kw">$(</span>greeting<span class="kw">)</span><span class="string">, </span><span class="kw">$(</span>name<span class="kw">)</span><span class="string">!"</span>
|
||||
|
||||
<span class="comment"># …</span>
|
||||
|
||||
@ -82,7 +82,7 @@ Suppose I decide to extract the `welcome` function to the top level, so I can re
|
||||
|
||||
<span class="comment"># …</span>
|
||||
|
||||
welcome <span class="kw">=</span> <span class="kw">\</span>prefix<span class="punctuation section">,</span> name <span class="kw">-></span> <span class="string">"</span><span class="kw">\(</span>prefix<span class="kw">)</span><span class="string">, </span><span class="kw">\(</span>name<span class="kw">)</span><span class="string">!"</span></samp></pre>
|
||||
welcome <span class="kw">=</span> <span class="kw">\</span>prefix<span class="punctuation section">,</span> name <span class="kw">-></span> <span class="string">"</span><span class="kw">$(</span>prefix<span class="kw">)</span><span class="string">, </span><span class="kw">$(</span>name<span class="kw">)</span><span class="string">!"</span></samp></pre>
|
||||
|
||||
Even without knowing the rest of `func`, we can be confident this change will not alter the code's behavior.
|
||||
|
||||
@ -90,7 +90,7 @@ In contrast, suppose Roc allowed reassignment. Then it's possible something in t
|
||||
|
||||
<pre><samp class="code-snippet">func <span class="kw">=</span> <span class="kw">\</span>arg <span class="kw">-></span>
|
||||
greeting <span class="kw">=</span> <span class="string">"Hello"</span>
|
||||
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-></span> <span class="string">"</span><span class="kw">\(</span>greeting<span class="kw">)</span><span class="string">, </span><span class="kw">\(</span>name<span class="kw">)</span><span class="string">!"</span>
|
||||
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-></span> <span class="string">"</span><span class="kw">$(</span>greeting<span class="kw">)</span><span class="string">, </span><span class="kw">$(</span>name<span class="kw">)</span><span class="string">!"</span>
|
||||
|
||||
<span class="comment"># …</span>
|
||||
|
||||
|
@ -146,6 +146,7 @@ If you would like your organization to become an official sponsor of Roc's devel
|
||||
We'd also like to express our gratitude to our generous [individual sponsors](https://github.com/sponsors/roc-lang/)! A special thanks to those sponsoring $25/month or more:
|
||||
|
||||
<ul id="individual-sponsors">
|
||||
<li><a href="https://github.com/megakilo">Steven Chen</a>
|
||||
<li><a href="https://github.com/asteroidb612">Drew Lazzeri</a>
|
||||
<li><a href="https://github.com/mrmizz">Alex Binaei</a>
|
||||
<li><a href="https://github.com/jonomallanyk">Jono Mallanyk</a>
|
||||
|
@ -1374,8 +1374,28 @@ pluralize = \singular, plural, count ->
|
||||
|
||||
This `expect` will fail if you call `pluralize` passing a count of 0.
|
||||
|
||||
Note that inline `expect`s do not halt the program! They are designed to inform, not to affect control flow. In fact, if you do `roc build`, they are not even included in the final binary.
|
||||
So you'll want to use `roc dev` or `roc test` to get the output for `expect`.
|
||||
Note that inline `expect`s do not halt the program! They are designed to inform, not to affect control flow. Different `roc` commands will also handle `expect`s differently:
|
||||
- `roc build` discards all `expect`s for optimal runtime performance.
|
||||
- `roc dev` only runs inline `expect`s that are encountered during normal execution of the program.
|
||||
- `roc test` runs top level `expect`s and inline `expect`s that are encountered because of the running of top level `expect`s.
|
||||
|
||||
Let's clear up any confusion with an example:
|
||||
```roc
|
||||
main =
|
||||
expect 1 == 2
|
||||
|
||||
Stdout.line "Hello, World!"
|
||||
|
||||
double = \num ->
|
||||
expect num > -1
|
||||
|
||||
num * 2
|
||||
|
||||
expect double 0 == 0
|
||||
```
|
||||
- `roc build` wil run `main`, ignore `expect 1 == 2` and just print `Hello, World!`.
|
||||
- `roc dev` will run `main`, tell you `expect 1 == 2` failed but will still print `Hello, World!`.
|
||||
- `roc test` will run `expect double 0 == 0` followed by `expect num > -1` and will print how many top level expects passed: `0 failed and 1 passed in 100 ms.`.
|
||||
|
||||
## [Modules](#modules) {#modules}
|
||||
|
||||
|
@ -66,7 +66,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/builtins/List#join">List.first</a></td>
|
||||
<td><a href="/builtins/List#first">List.first</a></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>head</li>
|
||||
|
@ -40,11 +40,11 @@ const repl = {
|
||||
},
|
||||
{
|
||||
match: (input) => input.replace(/ /g, "").match(/^name="/i),
|
||||
show: '<p>This created a new <a href="https://www.roc-lang.org/tutorial#defs">definition</a>—<code>name</code> is now defined to be equal to the <a href="/tutorial#strings-and-numbers">string</a> you entered.</p><p>Try using this definition by entering <code>"Hi, \\(name)!"</code></p>',
|
||||
show: '<p>This created a new <a href="https://www.roc-lang.org/tutorial#defs">definition</a>—<code>name</code> is now defined to be equal to the <a href="/tutorial#strings-and-numbers">string</a> you entered.</p><p>Try using this definition by entering <code>"Hi, \$(name)!"</code></p>',
|
||||
},
|
||||
{
|
||||
match: (input) => input.match(/^["][^\\]+\\\(name\)/i),
|
||||
show: `<p>Nicely done! This is an example of <a href=\"/tutorial#string-interpolation\">string interpolation</a>, which replaces part of a string with whatever you put inside the parentheses after a <code>\\</code>.</p><p>Now that you’ve written a few <a href=\"/tutorial#naming-things\">expressions</a>, you can either continue exploring in this REPL, or move on to the <a href=\"/tutorial\">tutorial</a> to learn how to make full programs.<p><p><span class='welcome-to-roc'>Welcome to Roc!</span> <a href='/tutorial' class='btn-small'>${tutorialButtonSvg} Start Tutorial</a></p>`,
|
||||
match: (input) => input.match(/^"[^\$]+\$\(name\)/i),
|
||||
show: `<p>Nicely done! This is an example of <a href=\"/tutorial#string-interpolation\">string interpolation</a>, which replaces part of a string with whatever you put inside the parentheses after a <code>$</code>.</p><p>Now that you’ve written a few <a href=\"/tutorial#naming-things\">expressions</a>, you can either continue exploring in this REPL, or move on to the <a href=\"/tutorial\">tutorial</a> to learn how to make full programs.<p><p><span class='welcome-to-roc'>Welcome to Roc!</span> <a href='/tutorial' class='btn-small'>${tutorialButtonSvg} Start Tutorial</a></p>`,
|
||||
},
|
||||
],
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user