Merge remote-tracking branch 'origin/main' into remove-nat

This commit is contained in:
Richard Feldman 2024-02-11 12:26:36 -05:00
commit 24a38c4a26
No known key found for this signature in database
GPG Key ID: F1F21AA5B1D9E43B
99 changed files with 2636 additions and 938 deletions

View File

@ -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
View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)"

View File

@ -10,7 +10,7 @@ show = \list ->
|> List.map Num.toStr
|> Str.joinWith ", "
"[\(content)]"
"[$(content)]"
sortBy : List a, (a -> Num *) -> List a
sortBy = \list, toComparable ->

View File

@ -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 ->

View File

@ -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

View File

@ -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"

View File

@ -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" }

View File

@ -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));
});
}

View File

@ -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;
}

View File

@ -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");

View File

@ -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 ->

View File

@ -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

View File

@ -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";

View File

@ -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,

View File

@ -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.

View File

@ -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 {

View File

@ -1831,7 +1831,7 @@ mod test_can {
// "abcd\$(efg)hij"
// "#
// ),
// Str(r"abcd\(efg)hij".into()),
// Str(r"abcd$(efg)hij".into()),
// );
// }

View File

@ -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
}

View File

@ -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"),
};

View File

@ -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,
};

View File

@ -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);
}

View File

@ -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(),
},

View File

@ -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;

View File

@ -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:

View File

@ -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);

View File

@ -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);
}

View File

@ -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.

View File

@ -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;

View File

@ -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,

View File

@ -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]),

View File

@ -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);
}
}

View File

@ -364,7 +364,7 @@ where
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct LineInfo {
line_offsets: Vec<u32>,
}

View File

@ -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
"#
),

View File

@ -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>"
"#

View File

@ -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"]

View File

@ -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() {

View File

@ -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)))"),

View 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;

View 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;

View File

@ -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 ():

View File

@ -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 {}
"
)
}

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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`

View File

@ -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

View File

@ -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()
}
}

View 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)
}
}
}

View 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)
}

View 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'|'.')
}

View File

@ -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))
}
}

View File

@ -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);
}
}

View File

@ -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(

View File

@ -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);
}

View File

@ -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))
}

View File

@ -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.

View File

@ -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),

View File

@ -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}");

View File

@ -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() {

View File

@ -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]

View File

@ -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
)

View File

@ -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.

View File

@ -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 = \_ ->

View File

@ -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))

View File

@ -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")

View File

@ -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!"

View File

@ -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 ->

View File

@ -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")

View File

@ -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

View File

@ -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 ->

View File

@ -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

View File

@ -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

View File

@ -5,4 +5,4 @@ app "libhello"
main : Str -> Str
main = \message ->
"TypeScript said to Roc: \(message)! 🎉"
"TypeScript said to Roc: $(message)! 🎉"

View File

@ -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"

View File

@ -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 ->

View File

@ -10,4 +10,4 @@ main = \num ->
else
str = Num.toStr num
"The number was \(str), OH YEAH!!! 🤘🤘"
"The number was $(str), OH YEAH!!! 🤘🤘"

View File

@ -10,4 +10,4 @@ main = \num ->
else
str = Num.toStr num
"The number was \(str), OH YEAH!!! 🤘🤘"
"The number was $(str), OH YEAH!!! 🤘🤘"

View File

@ -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
```

View File

@ -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 ->

View File

@ -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"

View File

@ -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"

View File

@ -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]
)
}
}

View File

@ -28,7 +28,7 @@ render = \state ->
head [] [],
body [] [
h1 [] [text "The app"],
div [] [text "The answer is \(num)"],
div [] [text "The answer is $(num)"],
],
]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -36,7 +36,7 @@ view =
Newline,
Desc [Ident "user", Kw "&lt;-", 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 "&lt;-", 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>
"""

View File

@ -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

View File

@ -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!

View File

@ -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">-&gt;</span>
greeting <span class="kw">=</span> <span class="string">"Hello"</span>
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-&gt;</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">-&gt;</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">-&gt;</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">-&gt;</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">-&gt;</span>
greeting <span class="kw">=</span> <span class="string">"Hello"</span>
welcome <span class="kw">=</span> <span class="kw">\</span>name <span class="kw">-&gt;</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">-&gt;</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>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>&mdash;<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>&mdash;<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 youve 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 youve 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>`,
},
],