Merge remote-tracking branch 'origin/main' into abilities-syntax

This commit is contained in:
Richard Feldman 2023-08-10 20:29:27 -04:00
commit 2da41be29f
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
524 changed files with 47536 additions and 15089 deletions

View file

@ -35,6 +35,7 @@ fn main() {
generate_bc_file(&bitcode_path, "ir-i386", "builtins-i386");
generate_bc_file(&bitcode_path, "ir-x86_64", "builtins-x86_64");
generate_bc_file(&bitcode_path, "ir-aarch64", "builtins-aarch64");
generate_bc_file(
&bitcode_path,
"ir-windows-x86_64",
@ -61,12 +62,12 @@ fn generate_bc_file(bitcode_path: &Path, zig_object: &str, file_name: &str) {
ll_path.set_extension("ll");
let dest_ir_host = ll_path.to_str().expect("Invalid dest ir path");
println!("Compiling host ir to: {}", dest_ir_host);
println!("Compiling host ir to: {dest_ir_host}");
let mut bc_path = bitcode_path.join(file_name);
bc_path.set_extension("bc");
let dest_bc_64bit = bc_path.to_str().expect("Invalid dest bc path");
println!("Compiling 64-bit bitcode to: {}", dest_bc_64bit);
println!("Compiling 64-bit bitcode to: {dest_bc_64bit}");
// workaround for github.com/ziglang/zig/issues/9711
#[cfg(target_os = "macos")]
@ -104,7 +105,7 @@ fn run_command(mut command: Command, flaky_fail_counter: usize) {
false => {
let error_str = match str::from_utf8(&output.stderr) {
Ok(stderr) => stderr.to_string(),
Err(_) => format!("Failed to run \"{}\"", command_str),
Err(_) => format!("Failed to run \"{command_str}\""),
};
// Flaky test errors that only occur sometimes on MacOS ci server.

View file

@ -66,7 +66,7 @@ fn generate_object_file(bitcode_path: &Path, zig_object: &str, object_file_name:
let src_obj_path = bitcode_path.join(object_file_name);
let src_obj = src_obj_path.to_str().expect("Invalid src object path");
println!("Compiling zig object `{}` to: {}", zig_object, src_obj);
println!("Compiling zig object `{zig_object}` to: {src_obj}");
if !DEBUG {
let mut zig_cmd = zig();
@ -77,7 +77,7 @@ fn generate_object_file(bitcode_path: &Path, zig_object: &str, object_file_name:
run_command(zig_cmd, 0);
println!("Moving zig object `{}` to: {}", zig_object, dest_obj);
println!("Moving zig object `{zig_object}` to: {dest_obj}");
// we store this .o file in rust's `target` folder (for wasm we need to leave a copy here too)
fs::copy(src_obj, dest_obj).unwrap_or_else(|err| {
@ -167,7 +167,7 @@ fn run_command(mut command: Command, flaky_fail_counter: usize) {
false => {
let error_str = match str::from_utf8(&output.stderr) {
Ok(stderr) => stderr.to_string(),
Err(_) => format!("Failed to run \"{}\"", command_str),
Err(_) => format!("Failed to run \"{command_str}\""),
};
// Flaky test errors that only occur sometimes on MacOS ci server.

View file

@ -25,18 +25,19 @@ pub fn build(b: *Builder) void {
const host_target = b.standardTargetOptions(.{
.default_target = CrossTarget{
.cpu_model = .baseline,
// TODO allow for native target for maximum speed
},
});
const linux32_target = makeLinux32Target();
const linux64_target = makeLinux64Target();
const linux_x64_target = makeLinuxX64Target();
const linux_aarch64_target = makeLinuxAarch64Target();
const windows64_target = makeWindows64Target();
const wasm32_target = makeWasm32Target();
// LLVM IR
generateLlvmIrFile(b, mode, host_target, main_path, "ir", "builtins-host");
generateLlvmIrFile(b, mode, linux32_target, main_path, "ir-i386", "builtins-i386");
generateLlvmIrFile(b, mode, linux64_target, main_path, "ir-x86_64", "builtins-x86_64");
generateLlvmIrFile(b, mode, linux_x64_target, main_path, "ir-x86_64", "builtins-x86_64");
generateLlvmIrFile(b, mode, linux_aarch64_target, main_path, "ir-aarch64", "builtins-aarch64");
generateLlvmIrFile(b, mode, windows64_target, main_path, "ir-windows-x86_64", "builtins-windows-x86_64");
generateLlvmIrFile(b, mode, wasm32_target, main_path, "ir-wasm32", "builtins-wasm32");
@ -89,6 +90,7 @@ fn generateObjectFile(
obj.strip = true;
obj.target = target;
obj.link_function_sections = true;
obj.force_pic = true;
const obj_step = b.step(step_name, "Build object file for linking");
obj_step.dependOn(&obj.step);
}
@ -103,7 +105,17 @@ fn makeLinux32Target() CrossTarget {
return target;
}
fn makeLinux64Target() CrossTarget {
fn makeLinuxAarch64Target() CrossTarget {
var target = CrossTarget.parse(.{}) catch unreachable;
target.cpu_arch = std.Target.Cpu.Arch.aarch64;
target.os_tag = std.Target.Os.Tag.linux;
target.abi = std.Target.Abi.musl;
return target;
}
fn makeLinuxX64Target() CrossTarget {
var target = CrossTarget.parse(.{}) catch unreachable;
target.cpu_arch = std.Target.Cpu.Arch.x86_64;

View file

@ -18,6 +18,7 @@ const v2u64 = @Vector(2, u64);
// Export it as weak incase it is already linked in by something else.
comptime {
@export(__muloti4, .{ .name = "__muloti4", .linkage = .Weak });
@export(__lshrti3, .{ .name = "__lshrti3", .linkage = .Weak });
if (want_windows_v2u64_abi) {
@export(__divti3_windows_x86_64, .{ .name = "__divti3", .linkage = .Weak });
@export(__modti3_windows_x86_64, .{ .name = "__modti3", .linkage = .Weak });
@ -440,3 +441,47 @@ pub inline fn floatFractionalBits(comptime T: type) comptime_int {
else => @compileError("unknown floating point type " ++ @typeName(T)),
};
}
pub fn __lshrti3(a: i128, b: i32) callconv(.C) i128 {
return lshrXi3(i128, a, b);
}
// Logical shift right: shift in 0 from left to right
// Precondition: 0 <= b < T.bit_count
inline fn lshrXi3(comptime T: type, a: T, b: i32) T {
const word_t = HalveInt(T, false);
const S = std.math.Log2Int(word_t.HalfT);
const input = word_t{ .all = a };
var output: word_t = undefined;
if (b >= word_t.bits) {
output.s.high = 0;
output.s.low = input.s.high >> @intCast(S, b - word_t.bits);
} else if (b == 0) {
return a;
} else {
output.s.high = input.s.high >> @intCast(S, b);
output.s.low = input.s.high << @intCast(S, word_t.bits - b);
output.s.low |= input.s.low >> @intCast(S, b);
}
return output.all;
}
/// Allows to access underlying bits as two equally sized lower and higher
/// signed or unsigned integers.
fn HalveInt(comptime T: type, comptime signed_half: bool) type {
return extern union {
pub const bits = @divExact(@typeInfo(T).Int.bits, 2);
pub const HalfTU = std.meta.Int(.unsigned, bits);
pub const HalfTS = std.meta.Int(.signed, bits);
pub const HalfT = if (signed_half) HalfTS else HalfTU;
all: T,
s: if (native_endian == .Little)
extern struct { low: HalfT, high: HalfT }
else
extern struct { high: HalfT, low: HalfT },
};
}

View file

@ -171,8 +171,8 @@ comptime {
exportStrFn(str.fromUtf8RangeC, "from_utf8_range");
exportStrFn(str.repeat, "repeat");
exportStrFn(str.strTrim, "trim");
exportStrFn(str.strTrimLeft, "trim_left");
exportStrFn(str.strTrimRight, "trim_right");
exportStrFn(str.strTrimStart, "trim_start");
exportStrFn(str.strTrimEnd, "trim_end");
exportStrFn(str.strCloneTo, "clone_to");
exportStrFn(str.withCapacity, "with_capacity");
exportStrFn(str.strGraphemes, "graphemes");
@ -195,8 +195,10 @@ comptime {
exportUtilsFn(utils.test_panic, "test_panic");
exportUtilsFn(utils.increfRcPtrC, "incref_rc_ptr");
exportUtilsFn(utils.decrefRcPtrC, "decref_rc_ptr");
exportUtilsFn(utils.freeRcPtrC, "free_rc_ptr");
exportUtilsFn(utils.increfDataPtrC, "incref_data_ptr");
exportUtilsFn(utils.decrefDataPtrC, "decref_data_ptr");
exportUtilsFn(utils.freeDataPtrC, "free_data_ptr");
exportUtilsFn(utils.isUnique, "is_unique");
exportUtilsFn(utils.decrefCheckNullC, "decref_check_null");
exportUtilsFn(utils.allocateWithRefcountC, "allocate_with_refcount");

View file

@ -2302,7 +2302,7 @@ pub fn strTrim(input_string: RocStr) callconv(.C) RocStr {
}
}
pub fn strTrimLeft(input_string: RocStr) callconv(.C) RocStr {
pub fn strTrimStart(input_string: RocStr) callconv(.C) RocStr {
var string = input_string;
if (string.isEmpty()) {
@ -2350,7 +2350,7 @@ pub fn strTrimLeft(input_string: RocStr) callconv(.C) RocStr {
}
}
pub fn strTrimRight(input_string: RocStr) callconv(.C) RocStr {
pub fn strTrimEnd(input_string: RocStr) callconv(.C) RocStr {
var string = input_string;
if (string.isEmpty()) {
@ -2583,22 +2583,22 @@ test "strTrim: small to small" {
try expect(trimmed.isSmallStr());
}
test "strTrimLeft: empty" {
const trimmedEmpty = strTrimLeft(RocStr.empty());
test "strTrimStart: empty" {
const trimmedEmpty = strTrimStart(RocStr.empty());
try expect(trimmedEmpty.eq(RocStr.empty()));
}
test "strTrimLeft: blank" {
test "strTrimStart: blank" {
const original_bytes = " ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
const trimmed = strTrimLeft(original);
const trimmed = strTrimStart(original);
try expect(trimmed.eq(RocStr.empty()));
}
test "strTrimLeft: large to large" {
test "strTrimStart: large to large" {
const original_bytes = " hello even more giant world ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
@ -2611,12 +2611,12 @@ test "strTrimLeft: large to large" {
try expect(!expected.isSmallStr());
const trimmed = strTrimLeft(original);
const trimmed = strTrimStart(original);
try expect(trimmed.eq(expected));
}
test "strTrimLeft: large to small" {
test "strTrimStart: large to small" {
// `original` will be consumed by the concat; do not free explicitly
const original_bytes = " hello ";
const original = RocStr.init(original_bytes, original_bytes.len);
@ -2629,14 +2629,14 @@ test "strTrimLeft: large to small" {
try expect(expected.isSmallStr());
const trimmed = strTrimLeft(original);
const trimmed = strTrimStart(original);
defer trimmed.decref();
try expect(trimmed.eq(expected));
try expect(!trimmed.isSmallStr());
}
test "strTrimLeft: small to small" {
test "strTrimStart: small to small" {
const original_bytes = " hello ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
@ -2649,28 +2649,28 @@ test "strTrimLeft: small to small" {
try expect(expected.isSmallStr());
const trimmed = strTrimLeft(original);
const trimmed = strTrimStart(original);
try expect(trimmed.eq(expected));
try expect(trimmed.isSmallStr());
}
test "strTrimRight: empty" {
const trimmedEmpty = strTrimRight(RocStr.empty());
test "strTrimEnd: empty" {
const trimmedEmpty = strTrimEnd(RocStr.empty());
try expect(trimmedEmpty.eq(RocStr.empty()));
}
test "strTrimRight: blank" {
test "strTrimEnd: blank" {
const original_bytes = " ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
const trimmed = strTrimRight(original);
const trimmed = strTrimEnd(original);
try expect(trimmed.eq(RocStr.empty()));
}
test "strTrimRight: large to large" {
test "strTrimEnd: large to large" {
const original_bytes = " hello even more giant world ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
@ -2683,12 +2683,12 @@ test "strTrimRight: large to large" {
try expect(!expected.isSmallStr());
const trimmed = strTrimRight(original);
const trimmed = strTrimEnd(original);
try expect(trimmed.eq(expected));
}
test "strTrimRight: large to small" {
test "strTrimEnd: large to small" {
// `original` will be consumed by the concat; do not free explicitly
const original_bytes = " hello ";
const original = RocStr.init(original_bytes, original_bytes.len);
@ -2701,14 +2701,14 @@ test "strTrimRight: large to small" {
try expect(expected.isSmallStr());
const trimmed = strTrimRight(original);
const trimmed = strTrimEnd(original);
defer trimmed.decref();
try expect(trimmed.eq(expected));
try expect(!trimmed.isSmallStr());
}
test "strTrimRight: small to small" {
test "strTrimEnd: small to small" {
const original_bytes = " hello ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
@ -2721,7 +2721,7 @@ test "strTrimRight: small to small" {
try expect(expected.isSmallStr());
const trimmed = strTrimRight(original);
const trimmed = strTrimEnd(original);
try expect(trimmed.eq(expected));
try expect(trimmed.isSmallStr());

View file

@ -220,6 +220,29 @@ pub fn increfDataPtrC(
return increfRcPtrC(isizes, inc_amount);
}
pub fn freeDataPtrC(
bytes_or_null: ?[*]isize,
alignment: u32,
) callconv(.C) void {
var bytes = bytes_or_null orelse return;
const ptr = @ptrToInt(bytes);
const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11;
const masked_ptr = ptr & ~tag_mask;
const isizes: [*]isize = @intToPtr([*]isize, masked_ptr);
return freeRcPtrC(isizes - 1, alignment);
}
pub fn freeRcPtrC(
bytes_or_null: ?[*]isize,
alignment: u32,
) callconv(.C) void {
var bytes = bytes_or_null orelse return;
return free_ptr_to_refcount(bytes, alignment);
}
pub fn decref(
bytes_or_null: ?[*]u8,
data_bytes: usize,
@ -236,13 +259,23 @@ pub fn decref(
decref_ptr_to_refcount(isizes - 1, alignment);
}
inline fn decref_ptr_to_refcount(
inline fn free_ptr_to_refcount(
refcount_ptr: [*]isize,
alignment: u32,
) void {
if (RC_TYPE == Refcount.none) return;
const extra_bytes = std.math.max(alignment, @sizeOf(usize));
// NOTE: we don't even check whether the refcount is "infinity" here!
dealloc(@ptrCast([*]u8, refcount_ptr) - (extra_bytes - @sizeOf(usize)), alignment);
}
inline fn decref_ptr_to_refcount(
refcount_ptr: [*]isize,
alignment: u32,
) void {
if (RC_TYPE == Refcount.none) return;
if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("| decrement {*}: ", .{refcount_ptr});
}
@ -264,13 +297,13 @@ inline fn decref_ptr_to_refcount(
}
if (refcount == REFCOUNT_ONE_ISIZE) {
dealloc(@ptrCast([*]u8, refcount_ptr) - (extra_bytes - @sizeOf(usize)), alignment);
free_ptr_to_refcount(refcount_ptr, alignment);
}
},
Refcount.atomic => {
var last = @atomicRmw(isize, &refcount_ptr[0], std.builtin.AtomicRmwOp.Sub, 1, Monotonic);
if (last == REFCOUNT_ONE_ISIZE) {
dealloc(@ptrCast([*]u8, refcount_ptr) - (extra_bytes - @sizeOf(usize)), alignment);
free_ptr_to_refcount(refcount_ptr, alignment);
}
},
Refcount.none => unreachable,

View file

@ -1,3 +1,7 @@
## Most users won't need Box, it is used for:
## - Holding unknown Roc types when developing [platforms](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform).
## - To improve performance in rare cases.
##
interface Box
exposes [box, unbox]
imports []

View file

@ -7,6 +7,7 @@ interface Dict
clear,
capacity,
len,
isEmpty,
get,
contains,
insert,
@ -21,6 +22,8 @@ interface Dict
insertAll,
keepShared,
removeAll,
map,
joinMap,
]
imports [
Bool.{ Bool, Eq },
@ -98,14 +101,14 @@ Dict k v := {
data : List (k, v),
size : Nat,
} where k implements Hash & Eq
implements [
Eq {
isEq,
},
Hash {
hash: hashDict,
},
]
implements [
Eq {
isEq,
},
Hash {
hash: hashDict,
},
]
isEq : Dict k v, Dict k v -> Bool where k implements Hash & Eq, v implements Eq
isEq = \xs, ys ->
@ -139,12 +142,12 @@ empty = \{} ->
## Returns the max number of elements the dictionary can hold before requiring a rehash.
## ```
## foodDict =
## Dict.empty {}
## |> Dict.insert "apple" "fruit"
## Dict.empty {}
## |> Dict.insert "apple" "fruit"
##
## capacityOfDict = Dict.capacity foodDict
## ```
capacity : Dict k v -> Nat where k implements Hash & Eq
capacity : Dict * * -> Nat
capacity = \@Dict { dataIndices } ->
cap = List.len dataIndices
@ -192,10 +195,20 @@ fromList = \data ->
## |> Dict.len
## |> Bool.isEq 3
## ```
len : Dict k v -> Nat where k implements Hash & Eq
len : Dict * * -> Nat
len = \@Dict { size } ->
size
## Check if the dictinoary is empty.
## ```
## Dict.isEmpty (Dict.empty {} |> Dict.insert "key" 42)
##
## Dict.isEmpty (Dict.empty {})
## ```
isEmpty : Dict * * -> Bool
isEmpty = \@Dict { size } ->
size == 0
## Clears all elements from a dictionary keeping around the allocation if it isn't huge.
## ```
## songs =
@ -225,6 +238,30 @@ clear = \@Dict { metadata, dataIndices, data } ->
size: 0,
}
## Convert each value in the dictionary to something new, by calling a conversion
## function on each of them which receives both the key and the old value. Then return a
## new dictionary containing the same keys and the converted values.
map : Dict k a, (k, a -> b) -> Dict k b
where k implements Hash & Eq, b implements Hash & Eq
map = \dict, transform ->
init = withCapacity (capacity dict)
walk dict init \answer, k, v ->
insert answer k (transform k v)
## Like [Dict.map], except the transformation function wraps the return value
## in a dictionary. At the end, all the dictionaries get joined together
## (using [Dict.insertAll]) into one dictionary.
##
## You may know a similar function named `concatMap` in other languages.
joinMap : Dict a b, (a, b -> Dict x y) -> Dict x y
where a implements Hash & Eq, x implements Hash & Eq
joinMap = \dict, transform ->
init = withCapacity (capacity dict) # Might be a pessimization
walk dict init \answer, k, v ->
insertAll answer (transform k v)
## Iterate through the keys and values in the dictionary and call the provided
## function with signature `state, k, v -> state` for each value, with an
## initial `state` value provided for the first call.
@ -976,16 +1013,16 @@ expect
# TODO: Add a builtin to distinguish big endian systems and change loading orders.
# TODO: Switch out Wymum on systems with slow 128bit multiplication.
LowLevelHasher := { originalSeed : U64, state : U64 } implements [
Hasher {
addBytes,
addU8,
addU16,
addU32,
addU64,
addU128,
complete,
},
]
Hasher {
addBytes,
addU8,
addU16,
addU32,
addU64,
addU128,
complete,
},
]
# unsafe primitive that does not perform a bounds check
# TODO hide behind an InternalList.roc module

View file

@ -208,6 +208,9 @@ interface List
## * Even when copying is faster, other list operations may still be slightly slower with persistent data structures. For example, even if it were a persistent data structure, [List.map], [List.walk], and [List.keepIf] would all need to traverse every element in the list and build up the result from scratch. These operations are all
## * Roc's compiler optimizes many list operations into in-place mutations behind the scenes, depending on how the list is being used. For example, [List.map], [List.keepIf], and [List.set] can all be optimized to perform in-place mutations.
## * If possible, it is usually best for performance to use large lists in a way where the optimizer can turn them into in-place mutations. If this is not possible, a persistent data structure might be faster - but this is a rare enough scenario that it would not be good for the average Roc program's performance if this were the way [List] worked by default. Instead, you can look outside Roc's standard modules for an implementation of a persistent data structure - likely built using [List] under the hood!
# separator so List.isEmpty doesn't absorb the above into its doc comment
## Check if the list is empty.
## ```
## List.isEmpty [1, 2, 3]
@ -222,6 +225,13 @@ isEmpty = \list ->
# but will cause a reference count increment on the value it got out of the list
getUnsafe : List a, Nat -> a
## Returns an element from a list at the given index.
##
## Returns `Err OutOfBounds` if the given index exceeds the List's length
## ```
## expect List.get [100, 200, 300] 1 == Ok 200
## expect List.get [100, 200, 300] 5 == Err OutOfBounds
## ```
get : List a, Nat -> Result a [OutOfBounds]
get = \list, index ->
if index < List.len list then
@ -352,6 +362,10 @@ releaseExcessCapacity : List a -> List a
concat : List a, List a -> List a
## Returns the last element in the list, or `ListWasEmpty` if it was empty.
## ```
## expect List.last [1, 2, 3] == Ok 3
## expect List.last [] == Err ListWasEmpty
## ```
last : List a -> Result a [ListWasEmpty]
last = \list ->
when List.get list (Num.subSaturated (List.len list) 1) is
@ -383,7 +397,7 @@ repeatHelp = \value, count, accum ->
## Returns the list with its elements reversed.
## ```
## List.reverse [1, 2, 3]
## expect List.reverse [1, 2, 3] == [3, 2, 1]
## ```
reverse : List a -> List a
reverse = \list ->
@ -397,9 +411,9 @@ reverseHelp = \list, left, right ->
## Join the given lists together into one list.
## ```
## List.join [[1, 2, 3], [4, 5], [], [6, 7]]
## List.join [[], []]
## List.join []
## expect List.join [[1], [2, 3], [], [4, 5]] == [1, 2, 3, 4, 5]
## expect List.join [[], []] == []
## expect List.join [] == []
## ```
join : List (List a) -> List a
join = \lists ->
@ -597,6 +611,10 @@ dropIf = \list, predicate ->
## Run the given function on each element of a list, and return the
## number of elements for which the function returned `Bool.true`.
## ```
## expect List.countIf [1, -2, -3] Num.isNegative == 2
## expect List.countIf [1, 2, 3] (\num -> num > 1 ) == 2
## ```
countIf : List a, (a -> Bool) -> Nat
countIf = \list, predicate ->
walkState = \state, elem ->
@ -610,11 +628,12 @@ countIf = \list, predicate ->
## This works like [List.map], except only the transformed values that are
## wrapped in `Ok` are kept. Any that are wrapped in `Err` are dropped.
## ```
## List.keepOks [["a", "b"], [], [], ["c", "d", "e"]] List.last
## expect List.keepOks ["1", "Two", "23", "Bird"] Str.toI32 == [1, 23]
##
## fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str)
## expect List.keepOks [["a", "b"], [], ["c", "d", "e"], [] ] List.first == ["a", "c"]
##
## List.keepOks ["", "a", "bc", "", "d", "ef", ""]
## fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok str
## expect List.keepOks ["", "a", "bc", "", "d", "ef", ""] fn == ["a", "bc", "d", "ef"]
## ```
keepOks : List before, (before -> Result after *) -> List after
keepOks = \list, toResult ->
@ -646,9 +665,9 @@ keepErrs = \list, toResult ->
## Convert each element in the list to something new, by calling a conversion
## function on each of them. Then return a new list of the converted values.
## ```
## List.map [1, 2, 3] (\num -> num + 1)
## expect List.map [1, 2, 3] (\num -> num + 1) == [2, 3, 4]
##
## List.map ["", "a", "bc"] Str.isEmpty
## expect List.map ["", "a", "bc"] Str.isEmpty == [Bool.true, Bool.false, Bool.false]
## ```
map : List a, (a -> b) -> List b
@ -675,6 +694,9 @@ map4 : List a, List b, List c, List d, (a, b, c, d -> e) -> List e
## This works like [List.map], except it also passes the index
## of the element to the conversion function.
## ```
## expect List.mapWithIndex [10, 20, 30] (\num, index -> num + index) == [10, 21, 32]
## ```
mapWithIndex : List a, (a, Nat -> b) -> List b
mapWithIndex = \src, func ->
length = len src
@ -1160,8 +1182,17 @@ mapTry = \list, toResult ->
Result.map (toResult elem) \ok ->
List.append state ok
## This is the same as `iterate` but with [Result] instead of `[Continue, Break]`.
## Using `Result` saves a conditional in `mapTry`.
## Same as [List.walk], except you can stop walking early by returning `Err`.
##
## ## Performance Details
##
## Compared to [List.walk], this can potentially visit fewer elements (which can
## improve performance) at the cost of making each step take longer.
## However, the added cost to each step is extremely small, and can easily
## be outweighed if it results in skipping even a small number of elements.
##
## As such, it is typically better for performance to use this over [List.walk]
## if returning `Break` earlier than the last element is expected to be common.
walkTry : List elem, state, (state, elem -> Result state err) -> Result state err
walkTry = \list, init, func ->
walkTryHelp list init func 0 (List.len list)

View file

@ -33,12 +33,17 @@ interface Num
Decimal,
Binary32,
Binary64,
e,
pi,
tau,
abs,
absDiff,
neg,
add,
sub,
mul,
min,
max,
isLt,
isLte,
isGt,
@ -81,6 +86,7 @@ interface Num
bitwiseAnd,
bitwiseXor,
bitwiseOr,
bitwiseNot,
shiftLeftBy,
shiftRightBy,
shiftRightZfBy,
@ -492,6 +498,18 @@ F32 : Num (FloatingPoint Binary32)
## [sqrt] and trigonometry are massively slower with [Dec] than with [F64].
Dec : Num (FloatingPoint Decimal)
## Euler's number (e)
e : Frac *
e = 2.71828182845904523536028747135266249775724709369995
## Archimedes' constant (π)
pi : Frac *
pi = 3.14159265358979323846264338327950288419716939937510
## Circle constant (τ)
tau : Frac *
tau = 2 * pi
# ------- Functions
## Convert a number to a [Str].
##
@ -777,6 +795,34 @@ sub : Num a, Num a -> Num a
## ∞ or -∞. For all other number types, overflow results in a panic.
mul : Num a, Num a -> Num a
## Obtains the smaller between two numbers of the same type.
##
## ```
## Num.min 100 0
##
## Num.min 3.0 -3.0
## ```
min : Num a, Num a -> Num a
min = \a, b ->
if a < b then
a
else
b
## Obtains the greater between two numbers of the same type.
##
## ```
## Num.max 100 0
##
## Num.max 3.0 -3.0
## ```
max : Num a, Num a -> Num a
max = \a, b ->
if a > b then
a
else
b
sin : Frac a -> Frac a
cos : Frac a -> Frac a
@ -930,10 +976,25 @@ remChecked = \a, b ->
isMultipleOf : Int a, Int a -> Bool
## Does a "bitwise and". Each bit of the output is 1 if the corresponding bit
## of x AND of y is 1, otherwise it's 0.
bitwiseAnd : Int a, Int a -> Int a
## Does a "bitwise exclusive or". Each bit of the output is the same as the
## corresponding bit in x if that bit in y is 0, and it's the complement of
## the bit in x if that bit in y is 1.
bitwiseXor : Int a, Int a -> Int a
## Does a "bitwise or". Each bit of the output is 0 if the corresponding bit
## of x OR of y is 0, otherwise it's 1.
bitwiseOr : Int a, Int a -> Int a
## Returns the complement of x - the number you get by switching each 1 for a
## 0 and each 0 for a 1. This is the same as -x - 1.
bitwiseNot : Int a -> Int a
bitwiseNot = \n ->
bitwiseXor n (subWrap 0 1)
## Bitwise left shift of a number by another
##
## The least significant bits always become 0. This means that shifting left is

View file

@ -42,8 +42,8 @@ withDefault = \result, default ->
## function on it. Then returns a new `Ok` holding the transformed value. If the
## result is `Err`, this has no effect. Use [mapErr] to transform an `Err`.
## ```
## Result.map (Ok 12) Num.negate
## Result.map (Err "yipes!") Num.negate
## Result.map (Ok 12) Num.neg
## Result.map (Err "yipes!") Num.neg
## ```
##
## Functions like `map` are common in Roc; see for example [List.map],

View file

@ -7,6 +7,8 @@ interface Set
walkUntil,
insert,
len,
isEmpty,
capacity,
remove,
contains,
toList,
@ -14,6 +16,8 @@ interface Set
union,
intersection,
difference,
map,
joinMap,
]
imports [
List,
@ -26,14 +30,14 @@ interface Set
## Provides a [set](https://en.wikipedia.org/wiki/Set_(abstract_data_type))
## type which stores a collection of unique values, without any ordering
Set k := Dict.Dict k {} where k implements Hash & Eq
implements [
Eq {
isEq,
},
Hash {
hash: hashSet,
},
]
implements [
Eq {
isEq,
},
Hash {
hash: hashSet,
},
]
isEq : Set k, Set k -> Bool where k implements Hash & Eq
isEq = \xs, ys ->
@ -59,6 +63,13 @@ hashSet = \hasher, @Set inner -> Hash.hash hasher inner
empty : {} -> Set k where k implements Hash & Eq
empty = \{} -> @Set (Dict.empty {})
## Return a dictionary with space allocated for a number of entries. This
## may provide a performance optimization if you know how many entries will be
## inserted.
withCapacity : Nat -> Set k where k implements Hash & Eq
withCapacity = \cap ->
@Set (Dict.withCapacity cap)
## Creates a new `Set` with a single value.
## ```
## singleItemSet = Set.single "Apple"
@ -115,10 +126,32 @@ expect
##
## expect countValues == 3
## ```
len : Set k -> Nat where k implements Hash & Eq
len : Set * -> Nat
len = \@Set dict ->
Dict.len dict
## Returns the max number of elements the set can hold before requiring a rehash.
## ```
## foodSet =
## Set.empty {}
## |> Set.insert "apple"
##
## capacityOfSet = Set.capacity foodSet
## ```
capacity : Set * -> Nat
capacity = \@Set dict ->
Dict.capacity dict
## Check if the set is empty.
## ```
## Set.isEmpty (Set.empty {} |> Set.insert 42)
##
## Set.isEmpty (Set.empty {})
## ```
isEmpty : Set * -> Bool
isEmpty = \@Set dict ->
Dict.isEmpty dict
# Inserting a duplicate key has no effect on length.
expect
actual =
@ -261,6 +294,28 @@ walk : Set k, state, (state, k -> state) -> state where k implements Hash & Eq
walk = \@Set dict, state, step ->
Dict.walk dict state (\s, k, _ -> step s k)
## Convert each value in the set to something new, by calling a conversion
## function on each of them which receives the old value. Then return a
## new set containing the converted values.
map : Set a, (a -> b) -> Set b where a implements Hash & Eq, b implements Hash & Eq
map = \set, transform ->
init = withCapacity (capacity set)
walk set init \answer, k ->
insert answer (transform k)
## Like [Set.map], except the transformation function wraps the return value
## in a set. At the end, all the sets get joined together
## (using [Set.union]) into one set.
##
## You may know a similar function named `concatMap` in other languages.
joinMap : Set a, (a -> Set b) -> Set b where a implements Hash & Eq, b implements Hash & Eq
joinMap = \set, transform ->
init = withCapacity (capacity set) # Might be a pessimization
walk set init \answer, k ->
union answer (transform k)
## Iterate through the values of a given `Set` and build a value, can stop
## iterating part way through the collection.
## ```

View file

@ -103,8 +103,8 @@ interface Str
startsWith,
endsWith,
trim,
trimLeft,
trimRight,
trimStart,
trimEnd,
toDec,
toF64,
toF32,
@ -448,15 +448,15 @@ trim : Str -> Str
## Return the [Str] with all whitespace removed from the beginning.
## ```
## expect Str.trimLeft " Hello \n\n" == "Hello \n\n"
## expect Str.trimStart " Hello \n\n" == "Hello \n\n"
## ```
trimLeft : Str -> Str
trimStart : Str -> Str
## Return the [Str] with all whitespace removed from the end.
## ```
## expect Str.trimRight " Hello \n\n" == " Hello"
## expect Str.trimEnd " Hello \n\n" == " Hello"
## ```
trimRight : Str -> Str
trimEnd : Str -> Str
## Encode a [Str] to a [Dec]. A [Dec] value is a 128-bit decimal
## [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic).
@ -638,12 +638,13 @@ countUtf8Bytes : Str -> Nat
substringUnsafe : Str, Nat, Nat -> Str
## Returns the given [Str] with each occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
## If the substring is not found, returns the original string.
##
## ```
## expect Str.replaceEach "foo/bar/baz" "/" "_" == Ok "foo_bar_baz"
## expect Str.replaceEach "not here" "/" "_" == Err NotFound
## expect Str.replaceEach "foo/bar/baz" "/" "_" == "foo_bar_baz"
## expect Str.replaceEach "not here" "/" "_" == "not here"
## ```
replaceEach : Str, Str, Str -> Result Str [NotFound]
replaceEach : Str, Str, Str -> Str
replaceEach = \haystack, needle, flower ->
when splitFirst haystack needle is
Ok { before, after } ->
@ -653,9 +654,8 @@ replaceEach = \haystack, needle, flower ->
|> Str.concat before
|> Str.concat flower
|> replaceEachHelp after needle flower
|> Ok
Err err -> Err err
Err NotFound -> haystack
replaceEachHelp : Str, Str, Str, Str -> Str
replaceEachHelp = \buf, haystack, needle, flower ->
@ -668,39 +668,44 @@ replaceEachHelp = \buf, haystack, needle, flower ->
Err NotFound -> Str.concat buf haystack
expect Str.replaceEach "abXdeXghi" "X" "_" == Ok "ab_de_ghi"
expect Str.replaceEach "abXdeXghi" "X" "_" == "ab_de_ghi"
expect Str.replaceEach "abcdefg" "nothing" "_" == "abcdefg"
## Returns the given [Str] with the first occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
## If the substring is not found, returns the original string.
##
## ```
## expect Str.replaceFirst "foo/bar/baz" "/" "_" == Ok "foo_bar/baz"
## expect Str.replaceFirst "no slashes here" "/" "_" == Err NotFound
## expect Str.replaceFirst "foo/bar/baz" "/" "_" == "foo_bar/baz"
## expect Str.replaceFirst "no slashes here" "/" "_" == "no slashes here"
## ```
replaceFirst : Str, Str, Str -> Result Str [NotFound]
replaceFirst : Str, Str, Str -> Str
replaceFirst = \haystack, needle, flower ->
when splitFirst haystack needle is
Ok { before, after } ->
Ok "\(before)\(flower)\(after)"
"\(before)\(flower)\(after)"
Err err -> Err err
Err NotFound -> haystack
expect Str.replaceFirst "abXdeXghi" "X" "_" == Ok "ab_deXghi"
expect Str.replaceFirst "abXdeXghi" "X" "_" == "ab_deXghi"
expect Str.replaceFirst "abcdefg" "nothing" "_" == "abcdefg"
## Returns the given [Str] with the last occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
## If the substring is not found, returns the original string.
##
## ```
## expect Str.replaceLast "foo/bar/baz" "/" "_" == Ok "foo/bar_baz"
## expect Str.replaceLast "no slashes here" "/" "_" == Err NotFound
## expect Str.replaceLast "foo/bar/baz" "/" "_" == "foo/bar_baz"
## expect Str.replaceLast "no slashes here" "/" "_" == "no slashes here"
## ```
replaceLast : Str, Str, Str -> Result Str [NotFound]
replaceLast : Str, Str, Str -> Str
replaceLast = \haystack, needle, flower ->
when splitLast haystack needle is
Ok { before, after } ->
Ok "\(before)\(flower)\(after)"
"\(before)\(flower)\(after)"
Err err -> Err err
Err NotFound -> haystack
expect Str.replaceLast "abXdeXghi" "X" "_" == Ok "abXde_ghi"
expect Str.replaceLast "abXdeXghi" "X" "_" == "abXde_ghi"
expect Str.replaceLast "abcdefg" "nothing" "_" == "abcdefg"
## Returns the given [Str] before the first occurrence of a [delimiter](https://www.computerhope.com/jargon/d/delimite.htm), as well
## as the rest of the string after that occurrence.

View file

@ -44,49 +44,49 @@ interface TotallyNotJson
## An opaque type with the `EncoderFormatting` and
## `DecoderFormatting` abilities.
Json := { fieldNameMapping : FieldNameMapping }
implements [
EncoderFormatting {
u8: encodeU8,
u16: encodeU16,
u32: encodeU32,
u64: encodeU64,
u128: encodeU128,
i8: encodeI8,
i16: encodeI16,
i32: encodeI32,
i64: encodeI64,
i128: encodeI128,
f32: encodeF32,
f64: encodeF64,
dec: encodeDec,
bool: encodeBool,
string: encodeString,
list: encodeList,
record: encodeRecord,
tuple: encodeTuple,
tag: encodeTag,
},
DecoderFormatting {
u8: decodeU8,
u16: decodeU16,
u32: decodeU32,
u64: decodeU64,
u128: decodeU128,
i8: decodeI8,
i16: decodeI16,
i32: decodeI32,
i64: decodeI64,
i128: decodeI128,
f32: decodeF32,
f64: decodeF64,
dec: decodeDec,
bool: decodeBool,
string: decodeString,
list: decodeList,
record: decodeRecord,
tuple: decodeTuple,
},
]
implements [
EncoderFormatting {
u8: encodeU8,
u16: encodeU16,
u32: encodeU32,
u64: encodeU64,
u128: encodeU128,
i8: encodeI8,
i16: encodeI16,
i32: encodeI32,
i64: encodeI64,
i128: encodeI128,
f32: encodeF32,
f64: encodeF64,
dec: encodeDec,
bool: encodeBool,
string: encodeString,
list: encodeList,
record: encodeRecord,
tuple: encodeTuple,
tag: encodeTag,
},
DecoderFormatting {
u8: decodeU8,
u16: decodeU16,
u32: decodeU32,
u64: decodeU64,
u128: decodeU128,
i8: decodeI8,
i16: decodeI16,
i32: decodeI32,
i64: decodeI64,
i128: decodeI128,
f32: decodeF32,
f64: decodeF64,
dec: decodeDec,
bool: decodeBool,
string: decodeString,
list: decodeList,
record: decodeRecord,
tuple: decodeTuple,
},
]
## Returns a JSON `Encoder` and `Decoder`
json = @Json { fieldNameMapping: Default }

View file

@ -339,8 +339,8 @@ pub const STR_TO_UTF8: &str = "roc_builtins.str.to_utf8";
pub const STR_FROM_UTF8_RANGE: &str = "roc_builtins.str.from_utf8_range";
pub const STR_REPEAT: &str = "roc_builtins.str.repeat";
pub const STR_TRIM: &str = "roc_builtins.str.trim";
pub const STR_TRIM_LEFT: &str = "roc_builtins.str.trim_left";
pub const STR_TRIM_RIGHT: &str = "roc_builtins.str.trim_right";
pub const STR_TRIM_START: &str = "roc_builtins.str.trim_start";
pub const STR_TRIM_END: &str = "roc_builtins.str.trim_end";
pub const STR_GET_UNSAFE: &str = "roc_builtins.str.get_unsafe";
pub const STR_RESERVE: &str = "roc_builtins.str.reserve";
pub const STR_APPEND_SCALAR: &str = "roc_builtins.str.append_scalar";
@ -393,8 +393,10 @@ pub const UTILS_TEST_PANIC: &str = "roc_builtins.utils.test_panic";
pub const UTILS_ALLOCATE_WITH_REFCOUNT: &str = "roc_builtins.utils.allocate_with_refcount";
pub const UTILS_INCREF_RC_PTR: &str = "roc_builtins.utils.incref_rc_ptr";
pub const UTILS_DECREF_RC_PTR: &str = "roc_builtins.utils.decref_rc_ptr";
pub const UTILS_FREE_RC_PTR: &str = "roc_builtins.utils.free_rc_ptr";
pub const UTILS_INCREF_DATA_PTR: &str = "roc_builtins.utils.incref_data_ptr";
pub const UTILS_DECREF_DATA_PTR: &str = "roc_builtins.utils.decref_data_ptr";
pub const UTILS_FREE_DATA_PTR: &str = "roc_builtins.utils.free_data_ptr";
pub const UTILS_IS_UNIQUE: &str = "roc_builtins.utils.is_unique";
pub const UTILS_DECREF_CHECK_NULL: &str = "roc_builtins.utils.decref_check_null";
pub const UTILS_DICT_PSEUDO_SEED: &str = "roc_builtins.utils.dict_pseudo_seed";