Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 87 additions & 42 deletions src/build/roc/Builtin.roc
Original file line number Diff line number Diff line change
Expand Up @@ -4717,20 +4717,57 @@ Builtin :: [].{
## If given a unique list, [List.keep_if] will mutate it in place to assemble the appropriate list.
## If that happens, this function will not allocate any new memory on the heap.
## If all items in the list end up being kept, Roc will return the original
## list unaltered.
## list unaltered. Otherwise, a shared list is copied into one new list with
## the original's length as its capacity.
##
keep_if : List(a), (a -> Bool) -> List(a)
keep_if = |list, predicate|
List.fold(
list,
[],
|acc, item|
if predicate(item) {
List.concat(acc, [item])
keep_if = |list, predicate| {
prepared = list_map_prepare_reuse(list)
len = prepared.len()
match list_owned_unique(prepared) {
1 => {
# Swap each kept item down to the end of the kept prefix, so the
# kept items stay in order and the dropped ones collect behind
# them for drop_last to release.
var $out = prepared
var $kept = 0
var $index = 0
while $index < len {
if predicate(list_get_unsafe($out, $index)) {
if $kept != $index {
$out = list_swap_unsafe($out, $kept, $index)
}
$kept = $kept + 1
}
$index = $index + 1
}
List.drop_last($out, len - $kept)
}
_ => {
# A shared list is returned as it is when every item is kept.
var $first_dropped = 0
while $first_dropped < len and predicate(list_get_unsafe(prepared, $first_dropped)) {
$first_dropped = $first_dropped + 1
}
if $first_dropped == len {
prepared
} else {
acc
},
)
# The items before $first_dropped were already tested and kept,
# so copy them in one step and test only the items after it.
var $new_list = list_append_sublist(List.with_capacity(len), prepared, 0, $first_dropped)
var $index = $first_dropped + 1
while $index < len {
item = list_get_unsafe(prepared, $index)
if predicate(item) {
$new_list = list_append_unsafe($new_list, item)
}
$index = $index + 1
}
$new_list
}
}
}
}

## Run the given function on each item of a list, and return all the
## items for which the function returned `Bool.False`.
Expand All @@ -4742,17 +4779,7 @@ Builtin :: [].{
## `List.drop_if` has the same performance characteristics as [List.keep_if].
## See its documentation for details on those characteristics!
drop_if : List(a), (a -> Bool) -> List(a)
drop_if = |list, predicate|
List.fold(
list,
[],
|acc, item|
if predicate(item) {
acc
} else {
List.concat(acc, [item])
},
)
drop_if = |list, predicate| List.keep_if(list, |item| !predicate(item))

## Run the given function on each item of a list, and return a list of
## the values it wrapped in `Ok`. Items the function maps to `Err` are
Expand All @@ -4761,16 +4788,25 @@ Builtin :: [].{
## expect [1.I64, 2, 3, 4].keep_oks(|n| if n.is_even() { Ok(n) } else { Err({}) }) == [2, 4]
## ```
keep_oks : List(before), (before -> Try(after, _err)) -> List(after)
keep_oks = |list, transform|
List.fold(
list,
[],
|acc, item|
match transform(item) {
Ok(after) => List.concat(acc, [after])
Err(_) => acc
},
)
keep_oks = |list, transform| {
# Allocated when the first Ok value needs a place, so a list with no
# Ok value allocates nothing.
var $out = []
for item in list {
$out = match transform(item) {
Ok(after) => {
reserved = if $out.is_empty() {
List.with_capacity(list.len())
} else {
$out
}
list_append_unsafe(reserved, after)
}
Err(_) => $out
}
}
$out
}

## Run the given function on each item of a list, and return a list of
## the values it wrapped in `Err`. Items the function maps to `Ok` are
Expand All @@ -4779,16 +4815,25 @@ Builtin :: [].{
## expect [1.I64, 2, 3, 4].keep_errs(|n| if n.is_even() { Ok(n) } else { Err(n) }) == [1, 3]
## ```
keep_errs : List(before), (before -> Try(_ok, after)) -> List(after)
keep_errs = |list, transform|
List.fold(
list,
[],
|acc, item|
match transform(item) {
Err(after) => List.concat(acc, [after])
Ok(_) => acc
},
)
keep_errs = |list, transform| {
# Allocated when the first Err value needs a place, so a list with no
# Err value allocates nothing.
var $out = []
for item in list {
$out = match transform(item) {
Err(after) => {
reserved = if $out.is_empty() {
List.with_capacity(list.len())
} else {
$out
}
list_append_unsafe(reserved, after)
}
Ok(_) => $out
}
}
$out
}

## Run the given function on each item of a list, and return the
## number of items for which the function returned `Bool.True`.
Expand Down
2 changes: 2 additions & 0 deletions src/cli/test/parallel_cli_runner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3604,6 +3604,8 @@ const subcommand_cases = [_]CliCase{
.{ .id = 0, .suite = .subcommands, .name = "issue 11934: clear/refill preserves parameter capacity (speed)", .backend = .speed, .skip = .{ .windows = "test/alloc-count platform does not have Windows host libraries" }, .body = .{ .command = .{ .args = &.{ "--opt=speed", "--no-cache" }, .roc_file = "test/alloc-count/app_list_clear.roc", .contains = &.{.{ .stream = .stderr, .text = "refill length: 5000, refill allocations: 1" }}, .not_contains = &.{.{ .stream = .stderr, .text = "Expect failed" }} } } },
.{ .id = 0, .suite = .subcommands, .name = "issue 12306: List.sort_with does not heap-allocate (dev)", .backend = .dev, .skip = .{ .windows = "test/alloc-count platform does not have Windows host libraries" }, .body = .{ .command = .{ .args = &.{ "--opt=dev", "--no-cache" }, .roc_file = "test/alloc-count/app_list_sort_with.roc", .contains = &.{.{ .stream = .stderr, .text = "sorted length: 8, sort allocations: 0" }}, .not_contains = &.{.{ .stream = .stderr, .text = "Expect failed" }} } } },
.{ .id = 0, .suite = .subcommands, .name = "issue 12306: List.sort_with does not heap-allocate (speed)", .backend = .speed, .skip = .{ .windows = "test/alloc-count platform does not have Windows host libraries" }, .body = .{ .command = .{ .args = &.{ "--opt=speed", "--no-cache" }, .roc_file = "test/alloc-count/app_list_sort_with.roc", .contains = &.{.{ .stream = .stderr, .text = "sorted length: 8, sort allocations: 0" }}, .not_contains = &.{.{ .stream = .stderr, .text = "Expect failed" }} } } },
.{ .id = 0, .suite = .subcommands, .name = "issue 12644: List.keep_if compacts a unique list in place (dev)", .backend = .dev, .skip = .{ .windows = "test/alloc-count platform does not have Windows host libraries" }, .body = .{ .command = .{ .args = &.{ "--opt=dev", "--no-cache" }, .roc_file = "test/alloc-count/app_list_keep_if.roc", .contains = &.{ .{ .stream = .stderr, .text = "keep_if allocations: some 0, all 0, none 0, drop_if 0, strings 0, shared 1, shared all 0, shared none 1, keep_oks 1, keep_oks none 0" }, .{ .stream = .stderr, .text = "kept lengths: [6, 8, 0, 2, 7, 8, 0] 2 2 0, shared input sum: 156" } }, .not_contains = &.{.{ .stream = .stderr, .text = "Expect failed" }} } } },
.{ .id = 0, .suite = .subcommands, .name = "issue 12644: List.keep_if compacts a unique list in place (speed)", .backend = .speed, .skip = .{ .windows = "test/alloc-count platform does not have Windows host libraries" }, .body = .{ .command = .{ .args = &.{ "--opt=speed", "--no-cache" }, .roc_file = "test/alloc-count/app_list_keep_if.roc", .contains = &.{ .{ .stream = .stderr, .text = "keep_if allocations: some 0, all 0, none 0, drop_if 0, strings 0, shared 1, shared all 0, shared none 1, keep_oks 1, keep_oks none 0" }, .{ .stream = .stderr, .text = "kept lengths: [6, 8, 0, 2, 7, 8, 0] 2 2 0, shared input sum: 156" } }, .not_contains = &.{.{ .stream = .stderr, .text = "Expect failed" }} } } },
.{ .id = 0, .suite = .subcommands, .name = "issue 12306: sorts are stable permutations for every shape, length, and item kind (dev)", .backend = .dev, .skip = .{ .windows = "test/alloc-count platform does not have Windows host libraries" }, .body = .{ .command = .{ .args = &.{ "--opt=dev", "--no-cache" }, .roc_file = "test/alloc-count/app_sort_property.roc", .contains = &.{.{ .stream = .stderr, .text = "sort property checks: 296, failures: 0, lengths: 31 1000 1000, in-place allocations: 0 1 2" }} } } },
.{ .id = 0, .suite = .subcommands, .name = "issue 12306: sorts are stable permutations for every shape, length, and item kind (speed)", .backend = .speed, .skip = .{ .windows = "test/alloc-count platform does not have Windows host libraries" }, .body = .{ .command = .{ .args = &.{ "--opt=speed", "--no-cache" }, .roc_file = "test/alloc-count/app_sort_property.roc", .contains = &.{.{ .stream = .stderr, .text = "sort property checks: 296, failures: 0, lengths: 31 1000 1000, in-place allocations: 0 1 2" }} } } },
.{ .id = 0, .suite = .subcommands, .name = "issue 11965: appends after a non-inlined List.drop_last reuse the allocation (dev)", .backend = .dev, .skip = .{ .windows = "test/alloc-count platform does not have Windows host libraries" }, .body = .{ .command = .{ .args = &.{ "--opt=dev", "--no-cache" }, .roc_file = "test/alloc-count/app_drop_last_append.roc", .contains = &.{.{ .stream = .stderr, .text = "drop_last steps: 1024 1024, allocations: 1 1" }} } } },
Expand Down
177 changes: 177 additions & 0 deletions src/eval/test/eval_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,183 @@ const core_tests = [_]TestCase{
.expected = .{ .crash = {} },
},

// List.keep_if compacts a unique list in place. Kept items move down in
// their original order and the final truncation releases the dropped ones.
.{
.name = "inspect: List.keep_if in place keeps order",
.source = "List.keep_if(List.concat([1, 2, 3], [4, 5, 6]), |x| x != 2 and x != 5)",
.expected = .{ .inspect_str = "[1.0, 3.0, 4.0, 6.0]" },
},
.{
.name = "inspect: List.keep_if in place keeps every item",
.source = "List.keep_if(List.concat([1, 2], [3]), |_| True)",
.expected = .{ .inspect_str = "[1.0, 2.0, 3.0]" },
},
.{
.name = "inspect: List.keep_if in place keeps no item",
.source = "List.keep_if(List.concat([1, 2], [3]), |_| False)",
.expected = .{ .inspect_str = "[]" },
},
.{
.name = "inspect: List.keep_if over emptied unique list",
.source = "List.keep_if(List.drop_first(List.concat([1], [2]), 2), |_| True)",
.expected = .{ .inspect_str = "[]" },
},
// list_owned_unique answers 0 for a seamless slice at runtime, so this
// usually takes the copy path; either path must give the same result.
.{
.name = "inspect: List.keep_if over unique seamless slice",
.source = "List.keep_if(List.sublist(List.concat([1, 2], [3, 4]), { start: 1, len: 3 }), |x| x != 3)",
.expected = .{ .inspect_str = "[2.0, 4.0]" },
},
.{
.name = "inspect: List.keep_if leaves shared list unchanged",
.source =
\\{
\\ a = List.concat([1, 2], [3, 4])
\\ b = List.keep_if(a, |x| x != 2)
\\ (a, b)
\\}
,
.expected = .{ .inspect_str = "([1.0, 2.0, 3.0, 4.0], [1.0, 3.0, 4.0])" },
},
// The predicate captures the list, which holds its refcount above 1. An
// in-place compaction here would let the predicate see moved items.
.{
.name = "inspect: List.keep_if predicate capturing the filtered list",
.source =
\\{
\\ a = List.concat([5, 7], [5, 9])
\\ List.keep_if(a, |x| x != (a.get(0) ?? 0) and a.len() == 4)
\\}
,
.expected = .{ .inspect_str = "[7.0, 9.0]" },
},
.{
.name = "inspect: List.keep_if in place over unique list of heap strings",
.source = "List.keep_if(List.concat([Str.repeat(\"x\", 30), Str.repeat(\"y\", 30)], [Str.repeat(\"z\", 30)]), |s| !Str.starts_with(s, \"y\"))",
.expected = .{ .inspect_str = "[\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\"]" },
},
.{
.name = "inspect: List.keep_if leaves shared string list unchanged",
.source =
\\{
\\ a = List.concat([Str.repeat("x", 30)], [Str.repeat("y", 30)])
\\ b = List.keep_if(a, |s| Str.starts_with(s, "y"))
\\ (a, b)
\\}
,
.expected = .{ .inspect_str = "([\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\"], [\"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\"])" },
},
// A shared list is returned as it is while every item is kept. Once an
// item is dropped, the kept prefix and the kept items after it are copied.
.{
.name = "inspect: List.keep_if over shared list drops first, middle and last items",
.source =
\\{
\\ a = List.concat([1, 2, 3], [4, 5, 6])
\\ (
\\ a,
\\ List.keep_if(a, |x| x != 1),
\\ List.keep_if(a, |x| x != 3 and x != 4),
\\ List.keep_if(a, |x| x != 6),
\\ List.keep_if(a, |_| True),
\\ List.keep_if(a, |_| False),
\\ )
\\}
,
.expected = .{ .inspect_str = "([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2.0, 3.0, 4.0, 5.0, 6.0], [1.0, 2.0, 5.0, 6.0], [1.0, 2.0, 3.0, 4.0, 5.0], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [])" },
},
.{
.name = "inspect: List.keep_oks and List.keep_errs with nothing to keep",
.source =
\\{
\\ a = List.concat([Str.repeat("x", 30)], [Str.repeat("y", 30)])
\\ no_oks : List(Str)
\\ no_oks = List.keep_oks(a, |s| Err(s))
\\ no_errs : List(Str)
\\ no_errs = List.keep_errs(a, |s| Ok(s))
\\ (a, no_oks, no_errs)
\\}
,
.expected = .{ .inspect_str = "([\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\"], [], [])" },
},
// Two list literals and the concat allocate. Keeping every item of the
// shared list returns it, and keeping none copies into one allocation.
.{
.name = "allocation - List.keep_if over shared list keeping all or none",
.source =
\\{
\\ a = List.concat([1, 2, 3], [4, 5, 6])
\\ all = List.keep_if(a, |_| True)
\\ none = List.keep_if(a, |_| False)
\\ if all.len() == 6 and none.len() == 0 and a.len() == 6 {
\\ "ok"
\\ } else {
\\ "bad"
\\ }
\\}
,
.expected = .{ .allocations_at_most = .{
.output = "ok",
.max_allocations = 4,
} },
},
.{
.name = "inspect: List.drop_if in place keeps order",
.source = "List.drop_if(List.concat([1, 2, 3], [4, 5, 6]), |x| x == 2 or x == 5)",
.expected = .{ .inspect_str = "[1.0, 3.0, 4.0, 6.0]" },
},
.{
.name = "inspect: List.keep_oks and List.keep_errs keep order",
.source =
\\{
\\ a = List.concat([Str.repeat("x", 30), Str.repeat("y", 30)], [Str.repeat("z", 30)])
\\ oks = List.keep_oks(a, |s| if Str.starts_with(s, "y") { Err(s) } else { Ok(s) })
\\ errs = List.keep_errs(a, |s| if Str.starts_with(s, "y") { Err(s) } else { Ok(s) })
\\ (oks, errs)
\\}
,
.expected = .{ .inspect_str = "([\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\"], [\"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\"])" },
},
// Two list literals and the concat allocate; the in-place keep_if adds
// nothing. The copy path would allocate one more and fail this ceiling.
.{
.name = "allocation - List.keep_if compacts unique list in place",
.source =
\\{
\\ kept = List.keep_if(List.concat([1, 2, 3], [4, 5, 6]), |x| x != 2 and x != 5)
\\ if kept.len() == 4 and kept.first() == Ok(1) and kept.last() == Ok(6) {
\\ "ok"
\\ } else {
\\ "bad"
\\ }
\\}
,
.expected = .{ .allocations_at_most = .{
.output = "ok",
.max_allocations = 3,
} },
},
.{
.name = "allocation - List.keep_if compacts unique list in place, optimized",
.source =
\\{
\\ kept = List.keep_if(List.concat([1, 2, 3], [4, 5, 6]), |x| x != 2 and x != 5)
\\ if kept.len() == 4 and kept.first() == Ok(1) and kept.last() == Ok(6) {
\\ "ok"
\\ } else {
\\ "bad"
\\ }
\\}
,
.expected = .{ .allocations_at_most = .{
.output = "ok",
.max_allocations = 3,
.optimized = true,
} },
},

// Basic expressions and control flow
.{ .name = "inspect: integer literal", .source = "42", .expected = .{ .inspect_str = "42.0" } },
.{ .name = "inspect: negative integer literal", .source = "-1234", .expected = .{ .inspect_str = "-1234.0" } },
Expand Down
Loading
Loading