From bc031078e2cfb61d5e8171f876578f48e8ef91d6 Mon Sep 17 00:00:00 2001 From: Braeden Foster <99447151+braedenfoster@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:33:47 +1300 Subject: [PATCH 1/4] Compact a unique list in place in List.keep_if keep_if, drop_if, keep_oks and keep_errs folded over List.concat(acc, [item]), so keeping 12 items cost 13 allocations and 4 reallocations. keep_if now follows List.map. A unique list is compacted in place with list_swap_unsafe and truncated with List.drop_last, so it allocates nothing and keeps its capacity, as its docs promise. A shared list is copied into one List.with_capacity(len) allocation. drop_if calls keep_if. keep_oks and keep_errs change the item type, so they build their result in one List.with_capacity(len) allocation. An in-place version would need a primitive that shortens a list without releasing the slots its items were moved out of. The Builtin snapshot node ids moved with the new definitions. --- src/build/roc/Builtin.roc | 94 +++++++------ src/cli/test/parallel_cli_runner.zig | 2 + src/eval/test/eval_tests.zig | 123 ++++++++++++++++++ src/eval/test/rc_conformance_tests.zig | 49 +++++++ test/alloc-count/app_list_keep_if.roc | 90 +++++++++++++ test/snapshots/bang_on_numeric_literal.md | 2 +- test/snapshots/bool_closure_type_check.md | 2 +- .../ellipsis_expression_positions.md | 2 +- test/snapshots/eval/tuple_bool.md | 8 +- test/snapshots/expr/not_tag.md | 2 +- test/snapshots/expr/unary_not.md | 2 +- test/snapshots/expr/unary_op_not.md | 2 +- test/snapshots/multiline_string_complex.md | 2 +- 13 files changed, 327 insertions(+), 53 deletions(-) create mode 100644 test/alloc-count/app_list_keep_if.roc diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index eb0de2678ce..885bc4e61f0 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -4720,17 +4720,37 @@ Builtin :: [].{ ## list unaltered. ## keep_if : List(a), (a -> Bool) -> List(a) - keep_if = |list, predicate| - List.fold( - list, - [], - |acc, item| - if predicate(item) { - List.concat(acc, [item]) - } else { - acc - }, - ) + 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)) { + $out = list_swap_unsafe($out, $kept, $index) + $kept = $kept + 1 + } + $index = $index + 1 + } + List.drop_last($out, len - $kept) + } + _ => { + var $new_list = List.with_capacity(len) + for item in prepared { + if predicate(item) { + $new_list = list_append_unsafe($new_list, item) + } + } + $new_list + } + } + } ## Run the given function on each item of a list, and return all the ## items for which the function returned `Bool.False`. @@ -4742,17 +4762,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 @@ -4761,16 +4771,16 @@ 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| { + var $out = List.with_capacity(list.len()) + for item in list { + $out = match transform(item) { + Ok(after) => list_append_unsafe($out, 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 @@ -4779,16 +4789,16 @@ 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| { + var $out = List.with_capacity(list.len()) + for item in list { + $out = match transform(item) { + Err(after) => list_append_unsafe($out, 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`. diff --git a/src/cli/test/parallel_cli_runner.zig b/src/cli/test/parallel_cli_runner.zig index 09fe7f299bc..cd8fa9a30e6 100644 --- a/src/cli/test/parallel_cli_runner.zig +++ b/src/cli/test/parallel_cli_runner.zig @@ -3546,6 +3546,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 = "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, keep_oks 1" }, .{ .stream = .stderr, .text = "kept lengths: [6, 8, 0, 2, 7] 2 2, shared input sum: 156" } }, .not_contains = &.{.{ .stream = .stderr, .text = "Expect failed" }} } } }, + .{ .id = 0, .suite = .subcommands, .name = "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, keep_oks 1" }, .{ .stream = .stderr, .text = "kept lengths: [6, 8, 0, 2, 7] 2 2, 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" }} } } }, diff --git a/src/eval/test/eval_tests.zig b/src/eval/test/eval_tests.zig index c4ed547f5ba..bb657df69f5 100644 --- a/src/eval/test/eval_tests.zig +++ b/src/eval/test/eval_tests.zig @@ -1714,6 +1714,129 @@ 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 = "[]" }, + }, + // A unique seamless slice must not be compacted in place, because its + // buffer points into the middle of an allocation. + .{ + .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\"])" }, + }, + .{ + .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" } }, diff --git a/src/eval/test/rc_conformance_tests.zig b/src/eval/test/rc_conformance_tests.zig index b0802588513..6615f2af26d 100644 --- a/src/eval/test/rc_conformance_tests.zig +++ b/src/eval/test/rc_conformance_tests.zig @@ -688,6 +688,55 @@ const cases = [_]Case{ \\} , }, + .{ + .name = "list keep_if compacts or copies its input", + .source = + \\{ + \\ shared = List.concat( + \\ ["a list element long enough to allocate", "another list element long enough"], + \\ ["a third list element long enough to allocate"], + \\ ) + \\ holder = [shared, shared] + \\ unique_kept = List.keep_if( + \\ List.concat( + \\ ["a list element long enough to allocate", "another list element long enough"], + \\ ["a fourth list element long enough to allocate"], + \\ ), + \\ |text| text.count_utf8_bytes() > 33, + \\ ) + \\ unique_dropped = List.drop_if( + \\ List.concat( + \\ ["a list element long enough to allocate", "another list element long enough"], + \\ ["a fourth list element long enough to allocate"], + \\ ), + \\ |text| text.count_utf8_bytes() > 33, + \\ ) + \\ unique_none = List.keep_if( + \\ List.concat( + \\ ["a list element long enough to allocate", "another list element long enough"], + \\ ["a fourth list element long enough to allocate"], + \\ ), + \\ |_| False, + \\ ) + \\ shared_kept = List.keep_if(shared, |text| text.count_utf8_bytes() > 33) + \\ oks = List.keep_oks(shared, |text| if text.count_utf8_bytes() > 33 { Ok(text) } else { Err(text) }) + \\ errs = List.keep_errs( + \\ List.concat( + \\ ["a list element long enough to allocate", "another list element long enough"], + \\ ["a fourth list element long enough to allocate"], + \\ ), + \\ |text| if text.count_utf8_bytes() > 33 { Ok(text) } else { Err(text) }, + \\ ) + \\ List.len(unique_kept) + \\ + List.len(unique_dropped) + \\ + List.len(unique_none) + \\ + List.len(shared_kept) + \\ + List.len(oks) + \\ + List.len(errs) + \\ + List.len(holder) + \\} + , + }, .{ .name = "boxed values are allocated, read, and updated", .source = diff --git a/test/alloc-count/app_list_keep_if.roc b/test/alloc-count/app_list_keep_if.roc new file mode 100644 index 00000000000..f73f1669a2c --- /dev/null +++ b/test/alloc-count/app_list_keep_if.roc @@ -0,0 +1,90 @@ +app [run!] { pf: platform "./platform/main.roc" } + +import pf.Host + +# List.keep_if and List.drop_if compact a uniquely owned list in place, so +# they allocate nothing. A shared list, and keep_oks, whose item type +# changes, build their result in one allocation. +numbers : U64 -> List(U64) +numbers = |offset| { + var $list = List.with_capacity(8) + var $i = 0 + while $i < 8 { + $list = List.append($list, offset + $i) + $i = $i + 1 + } + $list +} + +words : U64 -> List(Str) +words = |offset| { + var $list = List.with_capacity(4) + var $i = 0 + while $i < 4 { + $list = List.append($list, Str.repeat("a word long enough for the heap", offset + $i)) + $i = $i + 1 + } + $list +} + +run! : Str => Str +run! = |input| { + offset = Str.count_utf8_bytes(input) + + some = numbers(offset) + some_before = Host.alloc_count!() + some_kept = List.keep_if(some, |n| n - offset != 1 and n - offset != 4) + some_allocs = Host.alloc_count!() - some_before + expect some_kept == [offset, offset + 2, offset + 3, offset + 5, offset + 6, offset + 7] + + all = numbers(offset) + all_before = Host.alloc_count!() + all_kept = List.keep_if(all, |n| n >= offset) + all_allocs = Host.alloc_count!() - all_before + expect List.len(all_kept) == 8 + expect List.capacity(all_kept) == 8 + + none = numbers(offset) + none_before = Host.alloc_count!() + none_kept = List.keep_if(none, |n| n < offset) + none_allocs = Host.alloc_count!() - none_before + expect none_kept == [] + expect List.capacity(none_kept) == 8 + + dropped = numbers(offset) + drop_before = Host.alloc_count!() + drop_kept = List.drop_if(dropped, |n| n - offset < 6) + drop_allocs = Host.alloc_count!() - drop_before + expect drop_kept == [offset + 6, offset + 7] + + strs = words(offset) + strs_before = Host.alloc_count!() + strs_kept = List.keep_if(strs, |s| Str.count_utf8_bytes(s) % 2 == 0) + strs_allocs = Host.alloc_count!() - strs_before + expect List.len(strs_kept) == 2 + + shared = numbers(offset) + shared_before = Host.alloc_count!() + shared_kept = List.keep_if(shared, |n| n - offset != 3) + shared_allocs = Host.alloc_count!() - shared_before + expect shared == numbers(offset) + expect shared_kept == [offset, offset + 1, offset + 2, offset + 4, offset + 5, offset + 6, offset + 7] + + oks = numbers(offset) + oks_before = Host.alloc_count!() + oks_kept = List.keep_oks( + oks, + |n| if n - offset > 5 { + Ok(n.to_str()) + } else { + Err(n) + }, + ) + oks_allocs = Host.alloc_count!() - oks_before + expect List.len(oks_kept) == 2 + + # --opt=speed drops expects, so the output reads `shared` after keep_if to + # keep it shared there too. + lengths = [some_kept, all_kept, none_kept, drop_kept, shared_kept].map(List.len) + "keep_if allocations: some ${some_allocs.to_str()}, all ${all_allocs.to_str()}, none ${none_allocs.to_str()}, drop_if ${drop_allocs.to_str()}, strings ${strs_allocs.to_str()}, shared ${shared_allocs.to_str()}, keep_oks ${oks_allocs.to_str()}\nkept lengths: ${Str.inspect(lengths)} ${List.len(strs_kept).to_str()} ${List.len(oks_kept).to_str()}, shared input sum: ${List.sum(shared).to_str()}" +} diff --git a/test/snapshots/bang_on_numeric_literal.md b/test/snapshots/bang_on_numeric_literal.md index aa8db1abc8e..fb871ec6690 100644 --- a/test/snapshots/bang_on_numeric_literal.md +++ b/test/snapshots/bang_on_numeric_literal.md @@ -46,7 +46,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call (constraint-fn-var 222) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-runtime-error (tag "erroneous_value_expr"))) ~~~ # TYPES diff --git a/test/snapshots/bool_closure_type_check.md b/test/snapshots/bool_closure_type_check.md index 6e4fe2af9c3..98dcbc71a18 100644 --- a/test/snapshots/bool_closure_type_check.md +++ b/test/snapshots/bool_closure_type_check.md @@ -38,7 +38,7 @@ NO CHANGE (args (p-assign (ident "x"))) (e-call (constraint-fn-var 219) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-lookup-local (p-assign (ident "x"))))) (e-tag (name "True"))) diff --git a/test/snapshots/ellipsis_expression_positions.md b/test/snapshots/ellipsis_expression_positions.md index b1139e7e2e3..24f319e8e40 100644 --- a/test/snapshots/ellipsis_expression_positions.md +++ b/test/snapshots/ellipsis_expression_positions.md @@ -1135,7 +1135,7 @@ lambda_arg = |f| f(|_| ...) (args (p-underscore)) (e-call (constraint-fn-var 658) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-not-implemented)))) (d-let (p-assign (ident "question")) diff --git a/test/snapshots/eval/tuple_bool.md b/test/snapshots/eval/tuple_bool.md index 2a978fa7b3d..98f2b301a1e 100644 --- a/test/snapshots/eval/tuple_bool.md +++ b/test/snapshots/eval/tuple_bool.md @@ -53,10 +53,10 @@ NO CHANGE (builtin) (e-tag (name "False"))) (e-call (constraint-fn-var 257) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-tag (name "True"))) (e-call (constraint-fn-var 262) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-tag (name "False"))) (e-if (if-branches @@ -71,14 +71,14 @@ NO CHANGE (if-branches (if-branch (e-call (constraint-fn-var 279) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-tag (name "True"))) (e-nominal-external (builtin) (e-tag (name "True"))))) (if-else (e-call (constraint-fn-var 290) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-tag (name "True"))))))) ~~~ # TYPES diff --git a/test/snapshots/expr/not_tag.md b/test/snapshots/expr/not_tag.md index 4c104bc2cdc..5d4aceff0ba 100644 --- a/test/snapshots/expr/not_tag.md +++ b/test/snapshots/expr/not_tag.md @@ -66,7 +66,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-tag (name "C") (args (e-num (value "2"))))) diff --git a/test/snapshots/expr/unary_not.md b/test/snapshots/expr/unary_not.md index ee1ab14c76a..6881a19cf4e 100644 --- a/test/snapshots/expr/unary_not.md +++ b/test/snapshots/expr/unary_not.md @@ -28,7 +28,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES diff --git a/test/snapshots/expr/unary_op_not.md b/test/snapshots/expr/unary_op_not.md index 381c8666375..72c5047680f 100644 --- a/test/snapshots/expr/unary_op_not.md +++ b/test/snapshots/expr/unary_op_not.md @@ -28,7 +28,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES diff --git a/test/snapshots/multiline_string_complex.md b/test/snapshots/multiline_string_complex.md index d79312d8951..d4d72a4706c 100644 --- a/test/snapshots/multiline_string_complex.md +++ b/test/snapshots/multiline_string_complex.md @@ -355,7 +355,7 @@ With multiple lines (e-string))) (field (name "e") (e-call (constraint-fn-var 372) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18250") (target-def "18250")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) (e-runtime-error (tag "erroneous_value_expr"))))))) (d-let (p-assign (ident "x")) From 98e273b22941eba8d91163a5bb6a2f2a05fc5d4d Mon Sep 17 00:00:00 2001 From: Braeden Foster <99447151+braedenfoster@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:04:42 +1300 Subject: [PATCH 2/4] Return a shared list from List.keep_if when every item is kept A shared or static list went through one List.with_capacity(len) allocation even when keep_if kept every item or none. The shared path now tests items until the first dropped one and returns the input when there is none. Otherwise it copies the kept items into a new list that it allocates only when the first kept item needs a place, so keeping nothing allocates nothing. keep_oks and keep_errs also allocate on their first kept value. The predicate still runs once per item: the copy loop does not test the items before the first dropped one again. --- src/build/roc/Builtin.roc | 56 ++++++++++++++++--- src/cli/test/parallel_cli_runner.zig | 4 +- src/eval/test/eval_tests.zig | 54 ++++++++++++++++++ src/eval/test/rc_conformance_tests.zig | 6 +- test/alloc-count/app_list_keep_if.roc | 24 +++++++- test/snapshots/bang_on_numeric_literal.md | 2 +- test/snapshots/bool_closure_type_check.md | 2 +- .../ellipsis_expression_positions.md | 2 +- test/snapshots/eval/tuple_bool.md | 8 +-- test/snapshots/expr/not_tag.md | 2 +- test/snapshots/expr/unary_not.md | 2 +- test/snapshots/expr/unary_op_not.md | 2 +- test/snapshots/multiline_string_complex.md | 2 +- 13 files changed, 140 insertions(+), 26 deletions(-) diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index 885bc4e61f0..09c8e5efc18 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -4741,13 +4741,33 @@ Builtin :: [].{ List.drop_last($out, len - $kept) } _ => { - var $new_list = List.with_capacity(len) - for item in prepared { - if predicate(item) { - $new_list = list_append_unsafe($new_list, item) + # 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 { + # Copy the kept items. The items before $first_dropped were + # already tested, and the copy is allocated only when the first + # kept item needs a place, so keeping nothing allocates nothing. + var $new_list = [] + var $index = 0 + while $index < len { + item = list_get_unsafe(prepared, $index) + if $index < $first_dropped or ($index > $first_dropped and predicate(item)) { + reserved = if $new_list.is_empty() { + List.with_capacity(len) + } else { + $new_list + } + $new_list = list_append_unsafe(reserved, item) + } + $index = $index + 1 } + $new_list } - $new_list } } } @@ -4772,10 +4792,19 @@ Builtin :: [].{ ## ``` keep_oks : List(before), (before -> Try(after, _err)) -> List(after) keep_oks = |list, transform| { - var $out = List.with_capacity(list.len()) + # 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) => list_append_unsafe($out, after) + Ok(after) => { + reserved = if $out.is_empty() { + List.with_capacity(list.len()) + } else { + $out + } + list_append_unsafe(reserved, after) + } Err(_) => $out } } @@ -4790,10 +4819,19 @@ Builtin :: [].{ ## ``` keep_errs : List(before), (before -> Try(_ok, after)) -> List(after) keep_errs = |list, transform| { - var $out = List.with_capacity(list.len()) + # 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) => list_append_unsafe($out, after) + Err(after) => { + reserved = if $out.is_empty() { + List.with_capacity(list.len()) + } else { + $out + } + list_append_unsafe(reserved, after) + } Ok(_) => $out } } diff --git a/src/cli/test/parallel_cli_runner.zig b/src/cli/test/parallel_cli_runner.zig index cd8fa9a30e6..8c17d011841 100644 --- a/src/cli/test/parallel_cli_runner.zig +++ b/src/cli/test/parallel_cli_runner.zig @@ -3546,8 +3546,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 = "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, keep_oks 1" }, .{ .stream = .stderr, .text = "kept lengths: [6, 8, 0, 2, 7] 2 2, shared input sum: 156" } }, .not_contains = &.{.{ .stream = .stderr, .text = "Expect failed" }} } } }, - .{ .id = 0, .suite = .subcommands, .name = "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, keep_oks 1" }, .{ .stream = .stderr, .text = "kept lengths: [6, 8, 0, 2, 7] 2 2, shared input sum: 156" } }, .not_contains = &.{.{ .stream = .stderr, .text = "Expect failed" }} } } }, + .{ .id = 0, .suite = .subcommands, .name = "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 0, 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 = "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 0, 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" }} } } }, diff --git a/src/eval/test/eval_tests.zig b/src/eval/test/eval_tests.zig index bb657df69f5..7dd74aad16b 100644 --- a/src/eval/test/eval_tests.zig +++ b/src/eval/test/eval_tests.zig @@ -1782,6 +1782,60 @@ const core_tests = [_]TestCase{ , .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 allocates nothing. + .{ + .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 = 3, + } }, + }, .{ .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)", diff --git a/src/eval/test/rc_conformance_tests.zig b/src/eval/test/rc_conformance_tests.zig index 6615f2af26d..5dde88a9ea9 100644 --- a/src/eval/test/rc_conformance_tests.zig +++ b/src/eval/test/rc_conformance_tests.zig @@ -719,7 +719,9 @@ const cases = [_]Case{ \\ |_| False, \\ ) \\ shared_kept = List.keep_if(shared, |text| text.count_utf8_bytes() > 33) - \\ oks = List.keep_oks(shared, |text| if text.count_utf8_bytes() > 33 { Ok(text) } else { Err(text) }) + \\ shared_all = List.keep_if(shared, |_| True) + \\ shared_none = List.keep_if(shared, |_| False) + \\ oks =List.keep_oks(shared, |text| if text.count_utf8_bytes() > 33 { Ok(text) } else { Err(text) }) \\ errs = List.keep_errs( \\ List.concat( \\ ["a list element long enough to allocate", "another list element long enough"], @@ -731,6 +733,8 @@ const cases = [_]Case{ \\ + List.len(unique_dropped) \\ + List.len(unique_none) \\ + List.len(shared_kept) + \\ + List.len(shared_all) + \\ + List.len(shared_none) \\ + List.len(oks) \\ + List.len(errs) \\ + List.len(holder) diff --git a/test/alloc-count/app_list_keep_if.roc b/test/alloc-count/app_list_keep_if.roc index f73f1669a2c..c28fd6fd652 100644 --- a/test/alloc-count/app_list_keep_if.roc +++ b/test/alloc-count/app_list_keep_if.roc @@ -4,7 +4,8 @@ import pf.Host # List.keep_if and List.drop_if compact a uniquely owned list in place, so # they allocate nothing. A shared list, and keep_oks, whose item type -# changes, build their result in one allocation. +# changes, build their result in one allocation, and allocate nothing when +# they keep nothing. A shared list that keeps every item comes back as it is. numbers : U64 -> List(U64) numbers = |offset| { var $list = List.with_capacity(8) @@ -70,6 +71,16 @@ run! = |input| { expect shared == numbers(offset) expect shared_kept == [offset, offset + 1, offset + 2, offset + 4, offset + 5, offset + 6, offset + 7] + shared_all_before = Host.alloc_count!() + shared_all_kept = List.keep_if(shared, |n| n >= offset) + shared_all_allocs = Host.alloc_count!() - shared_all_before + expect shared_all_kept == shared + + shared_none_before = Host.alloc_count!() + shared_none_kept = List.keep_if(shared, |n| n < offset) + shared_none_allocs = Host.alloc_count!() - shared_none_before + expect shared_none_kept == [] + oks = numbers(offset) oks_before = Host.alloc_count!() oks_kept = List.keep_oks( @@ -83,8 +94,15 @@ run! = |input| { oks_allocs = Host.alloc_count!() - oks_before expect List.len(oks_kept) == 2 + no_oks = numbers(offset) + no_oks_before = Host.alloc_count!() + no_oks_kept : List(Str) + no_oks_kept = List.keep_oks(no_oks, |n| Err(n)) + no_oks_allocs = Host.alloc_count!() - no_oks_before + expect no_oks_kept == [] + # --opt=speed drops expects, so the output reads `shared` after keep_if to # keep it shared there too. - lengths = [some_kept, all_kept, none_kept, drop_kept, shared_kept].map(List.len) - "keep_if allocations: some ${some_allocs.to_str()}, all ${all_allocs.to_str()}, none ${none_allocs.to_str()}, drop_if ${drop_allocs.to_str()}, strings ${strs_allocs.to_str()}, shared ${shared_allocs.to_str()}, keep_oks ${oks_allocs.to_str()}\nkept lengths: ${Str.inspect(lengths)} ${List.len(strs_kept).to_str()} ${List.len(oks_kept).to_str()}, shared input sum: ${List.sum(shared).to_str()}" + lengths = [some_kept, all_kept, none_kept, drop_kept, shared_kept, shared_all_kept, shared_none_kept].map(List.len) + "keep_if allocations: some ${some_allocs.to_str()}, all ${all_allocs.to_str()}, none ${none_allocs.to_str()}, drop_if ${drop_allocs.to_str()}, strings ${strs_allocs.to_str()}, shared ${shared_allocs.to_str()}, shared all ${shared_all_allocs.to_str()}, shared none ${shared_none_allocs.to_str()}, keep_oks ${oks_allocs.to_str()}, keep_oks none ${no_oks_allocs.to_str()}\nkept lengths: ${Str.inspect(lengths)} ${List.len(strs_kept).to_str()} ${List.len(oks_kept).to_str()} ${List.len(no_oks_kept).to_str()}, shared input sum: ${List.sum(shared).to_str()}" } diff --git a/test/snapshots/bang_on_numeric_literal.md b/test/snapshots/bang_on_numeric_literal.md index fb871ec6690..4c6b1a1039f 100644 --- a/test/snapshots/bang_on_numeric_literal.md +++ b/test/snapshots/bang_on_numeric_literal.md @@ -46,7 +46,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call (constraint-fn-var 222) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-runtime-error (tag "erroneous_value_expr"))) ~~~ # TYPES diff --git a/test/snapshots/bool_closure_type_check.md b/test/snapshots/bool_closure_type_check.md index 98dcbc71a18..cb7c9192e64 100644 --- a/test/snapshots/bool_closure_type_check.md +++ b/test/snapshots/bool_closure_type_check.md @@ -38,7 +38,7 @@ NO CHANGE (args (p-assign (ident "x"))) (e-call (constraint-fn-var 219) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-lookup-local (p-assign (ident "x"))))) (e-tag (name "True"))) diff --git a/test/snapshots/ellipsis_expression_positions.md b/test/snapshots/ellipsis_expression_positions.md index 24f319e8e40..6a57d548262 100644 --- a/test/snapshots/ellipsis_expression_positions.md +++ b/test/snapshots/ellipsis_expression_positions.md @@ -1135,7 +1135,7 @@ lambda_arg = |f| f(|_| ...) (args (p-underscore)) (e-call (constraint-fn-var 658) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-not-implemented)))) (d-let (p-assign (ident "question")) diff --git a/test/snapshots/eval/tuple_bool.md b/test/snapshots/eval/tuple_bool.md index 98f2b301a1e..6a4e1fb280c 100644 --- a/test/snapshots/eval/tuple_bool.md +++ b/test/snapshots/eval/tuple_bool.md @@ -53,10 +53,10 @@ NO CHANGE (builtin) (e-tag (name "False"))) (e-call (constraint-fn-var 257) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-tag (name "True"))) (e-call (constraint-fn-var 262) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-tag (name "False"))) (e-if (if-branches @@ -71,14 +71,14 @@ NO CHANGE (if-branches (if-branch (e-call (constraint-fn-var 279) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-tag (name "True"))) (e-nominal-external (builtin) (e-tag (name "True"))))) (if-else (e-call (constraint-fn-var 290) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-tag (name "True"))))))) ~~~ # TYPES diff --git a/test/snapshots/expr/not_tag.md b/test/snapshots/expr/not_tag.md index 5d4aceff0ba..a1a891b1cec 100644 --- a/test/snapshots/expr/not_tag.md +++ b/test/snapshots/expr/not_tag.md @@ -66,7 +66,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-tag (name "C") (args (e-num (value "2"))))) diff --git a/test/snapshots/expr/unary_not.md b/test/snapshots/expr/unary_not.md index 6881a19cf4e..ac9e23dbc5f 100644 --- a/test/snapshots/expr/unary_not.md +++ b/test/snapshots/expr/unary_not.md @@ -28,7 +28,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES diff --git a/test/snapshots/expr/unary_op_not.md b/test/snapshots/expr/unary_op_not.md index 72c5047680f..f83289c1348 100644 --- a/test/snapshots/expr/unary_op_not.md +++ b/test/snapshots/expr/unary_op_not.md @@ -28,7 +28,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES diff --git a/test/snapshots/multiline_string_complex.md b/test/snapshots/multiline_string_complex.md index d4d72a4706c..eb68de36e0b 100644 --- a/test/snapshots/multiline_string_complex.md +++ b/test/snapshots/multiline_string_complex.md @@ -355,7 +355,7 @@ With multiple lines (e-string))) (field (name "e") (e-call (constraint-fn-var 372) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18321") (target-def "18321")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) (e-runtime-error (tag "erroneous_value_expr"))))))) (d-let (p-assign (ident "x")) From cf5e60971aa631c59b83f7e6f150806c5411d8f4 Mon Sep 17 00:00:00 2001 From: Braeden Foster <99447151+braedenfoster@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:05:55 +1300 Subject: [PATCH 3/4] Update dev_object_float_math hashes for the new keep_if --- test/snapshots/dev_object_float_math.md | 62 ++++++++++++------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/test/snapshots/dev_object_float_math.md b/test/snapshots/dev_object_float_math.md index 0377c438b9e..4fd368a2c6a 100644 --- a/test/snapshots/dev_object_float_math.md +++ b/test/snapshots/dev_object_float_math.md @@ -51,37 +51,37 @@ main = |x, y, n| { ~~~ # DEV OUTPUT ~~~ini -x64mac=6fd5136f3792850017b66446ecf13dc17e6e079b8f64c80892afd438973dac06 -x64win=efe16a28d32e1f2b4cef3e899999ff76194a48ed8c0ec8f228b766dbe5475098 -x64mingw=efe16a28d32e1f2b4cef3e899999ff76194a48ed8c0ec8f228b766dbe5475098 -x64freebsd=115b22e345ac30f0236504b531fcec95eac7be53ceb4f3b472e6c4289ad7b6f9 -x64openbsd=815ff1c8c357a7619d79d34e497e516c74c2bb7218c77e720a86b43062279440 -x64netbsd=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64musl=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64glibc=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64linux=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64elf=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64v1mac=6fd5136f3792850017b66446ecf13dc17e6e079b8f64c80892afd438973dac06 -x64v1win=efe16a28d32e1f2b4cef3e899999ff76194a48ed8c0ec8f228b766dbe5475098 -x64v1mingw=efe16a28d32e1f2b4cef3e899999ff76194a48ed8c0ec8f228b766dbe5475098 -x64v1freebsd=115b22e345ac30f0236504b531fcec95eac7be53ceb4f3b472e6c4289ad7b6f9 -x64v1openbsd=815ff1c8c357a7619d79d34e497e516c74c2bb7218c77e720a86b43062279440 -x64v1netbsd=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64v1musl=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64v1glibc=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64v1linux=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -x64v1elf=d25a35d7e048dd89e17d5ce6790b564fbe4b3e99133e08fca0a4f27f4bca07f1 -arm64mac=7390125e5b782fb22c05cbe2e2de0a8b8df85b8c10f6dfcb94968ab6ec94ba74 -arm64win=1c4d0360a4a6122cebf7d34c3fabc34de2fd555393a90e0188ec33d4bfbf68c2 -arm64mingw=1c4d0360a4a6122cebf7d34c3fabc34de2fd555393a90e0188ec33d4bfbf68c2 -arm64linux=f0a0638e0cc6499f44ec49d0506e5d451a3578dcd4d5131e88aa5b2824a1144c -arm64musl=f0a0638e0cc6499f44ec49d0506e5d451a3578dcd4d5131e88aa5b2824a1144c -arm64glibc=f0a0638e0cc6499f44ec49d0506e5d451a3578dcd4d5131e88aa5b2824a1144c -arm64v1win=1c4d0360a4a6122cebf7d34c3fabc34de2fd555393a90e0188ec33d4bfbf68c2 -arm64v1mingw=1c4d0360a4a6122cebf7d34c3fabc34de2fd555393a90e0188ec33d4bfbf68c2 -arm64v1linux=f0a0638e0cc6499f44ec49d0506e5d451a3578dcd4d5131e88aa5b2824a1144c -arm64v1musl=f0a0638e0cc6499f44ec49d0506e5d451a3578dcd4d5131e88aa5b2824a1144c -arm64v1glibc=f0a0638e0cc6499f44ec49d0506e5d451a3578dcd4d5131e88aa5b2824a1144c +x64mac=0a6a8023cf9e6481c40547844ef5868916c50647b9cf83e427d42c27bb1a78aa +x64win=e552bb9aed9b18bc9ff852cd054a12cacef51e0395ffb988d84526ad058eaeed +x64mingw=e552bb9aed9b18bc9ff852cd054a12cacef51e0395ffb988d84526ad058eaeed +x64freebsd=28b28ae5b26fc1d309236b75febd0a87744a83df9a72ccabb06c9c6c4fb775cd +x64openbsd=877ea46139cc449b654c88b549662efe8ce1c56ccb94b997ca5ae494603d7faa +x64netbsd=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64musl=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64glibc=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64linux=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64elf=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64v1mac=0a6a8023cf9e6481c40547844ef5868916c50647b9cf83e427d42c27bb1a78aa +x64v1win=e552bb9aed9b18bc9ff852cd054a12cacef51e0395ffb988d84526ad058eaeed +x64v1mingw=e552bb9aed9b18bc9ff852cd054a12cacef51e0395ffb988d84526ad058eaeed +x64v1freebsd=28b28ae5b26fc1d309236b75febd0a87744a83df9a72ccabb06c9c6c4fb775cd +x64v1openbsd=877ea46139cc449b654c88b549662efe8ce1c56ccb94b997ca5ae494603d7faa +x64v1netbsd=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64v1musl=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64v1glibc=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64v1linux=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +x64v1elf=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f +arm64mac=4a3b95d88385254511c87faec3e78f9f5db70ab6586ad11caba8c604df09e9a7 +arm64win=9466e5f20613a29d6aa31e9afb3a10996b008b1419a037c40154afaeae004173 +arm64mingw=9466e5f20613a29d6aa31e9afb3a10996b008b1419a037c40154afaeae004173 +arm64linux=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 +arm64musl=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 +arm64glibc=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 +arm64v1win=9466e5f20613a29d6aa31e9afb3a10996b008b1419a037c40154afaeae004173 +arm64v1mingw=9466e5f20613a29d6aa31e9afb3a10996b008b1419a037c40154afaeae004173 +arm64v1linux=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 +arm64v1musl=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 +arm64v1glibc=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 arm32linux=NOT_IMPLEMENTED arm32musl=NOT_IMPLEMENTED wasm32=NOT_IMPLEMENTED From 4056509a4c5804fc1f7dc909ab4919c333ce4e39 Mon Sep 17 00:00:00 2001 From: Anton-4-bot <309861636+Anton-4-bot@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:17:26 +0100 Subject: [PATCH 4/4] Copy a shared list's kept prefix in one step in List.keep_if The shared path now allocates its List.with_capacity(len) copy up front, appends the items before the first dropped one with list_append_sublist, and tests only the items after it. This removes the per-item index check and the lazy-allocation test from the copy loop. Keeping nothing from a shared list whose first item is dropped now allocates once, which matches the documented "same memory as the original" behavior. The unique path skips list_swap_unsafe when an item is already in place. Also: document the shared-list copy in the keep_if docs, fix the seamless slice test comment, prefix the alloc-count CLI cases with the issue number, and fix a missing space in the rc_conformance test. Co-Authored-By: Claude Opus 5.5 --- src/build/roc/Builtin.roc | 25 ++++---- src/cli/test/parallel_cli_runner.zig | 4 +- src/eval/test/eval_tests.zig | 8 +-- src/eval/test/rc_conformance_tests.zig | 2 +- test/alloc-count/app_list_keep_if.roc | 4 +- test/snapshots/bang_on_numeric_literal.md | 2 +- test/snapshots/bool_closure_type_check.md | 2 +- test/snapshots/dev_object_float_math.md | 62 +++++++++---------- .../ellipsis_expression_positions.md | 2 +- test/snapshots/eval/tuple_bool.md | 8 +-- test/snapshots/expr/not_tag.md | 2 +- test/snapshots/expr/unary_not.md | 2 +- test/snapshots/expr/unary_op_not.md | 2 +- test/snapshots/multiline_string_complex.md | 2 +- 14 files changed, 62 insertions(+), 65 deletions(-) diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index 09c8e5efc18..0c5c3568ced 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -4717,7 +4717,8 @@ 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| { @@ -4733,7 +4734,9 @@ Builtin :: [].{ var $index = 0 while $index < len { if predicate(list_get_unsafe($out, $index)) { - $out = list_swap_unsafe($out, $kept, $index) + if $kept != $index { + $out = list_swap_unsafe($out, $kept, $index) + } $kept = $kept + 1 } $index = $index + 1 @@ -4749,20 +4752,14 @@ Builtin :: [].{ if $first_dropped == len { prepared } else { - # Copy the kept items. The items before $first_dropped were - # already tested, and the copy is allocated only when the first - # kept item needs a place, so keeping nothing allocates nothing. - var $new_list = [] - var $index = 0 + # 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 $index < $first_dropped or ($index > $first_dropped and predicate(item)) { - reserved = if $new_list.is_empty() { - List.with_capacity(len) - } else { - $new_list - } - $new_list = list_append_unsafe(reserved, item) + if predicate(item) { + $new_list = list_append_unsafe($new_list, item) } $index = $index + 1 } diff --git a/src/cli/test/parallel_cli_runner.zig b/src/cli/test/parallel_cli_runner.zig index 8c17d011841..402938b8b11 100644 --- a/src/cli/test/parallel_cli_runner.zig +++ b/src/cli/test/parallel_cli_runner.zig @@ -3546,8 +3546,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 = "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 0, 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 = "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 0, 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 (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" }} } } }, diff --git a/src/eval/test/eval_tests.zig b/src/eval/test/eval_tests.zig index 7dd74aad16b..cf896229e24 100644 --- a/src/eval/test/eval_tests.zig +++ b/src/eval/test/eval_tests.zig @@ -1736,8 +1736,8 @@ const core_tests = [_]TestCase{ .source = "List.keep_if(List.drop_first(List.concat([1], [2]), 2), |_| True)", .expected = .{ .inspect_str = "[]" }, }, - // A unique seamless slice must not be compacted in place, because its - // buffer points into the middle of an allocation. + // 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)", @@ -1816,7 +1816,7 @@ const core_tests = [_]TestCase{ .expected = .{ .inspect_str = "([\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\"], [], [])" }, }, // Two list literals and the concat allocate. Keeping every item of the - // shared list returns it, and keeping none allocates nothing. + // shared list returns it, and keeping none copies into one allocation. .{ .name = "allocation - List.keep_if over shared list keeping all or none", .source = @@ -1833,7 +1833,7 @@ const core_tests = [_]TestCase{ , .expected = .{ .allocations_at_most = .{ .output = "ok", - .max_allocations = 3, + .max_allocations = 4, } }, }, .{ diff --git a/src/eval/test/rc_conformance_tests.zig b/src/eval/test/rc_conformance_tests.zig index 5dde88a9ea9..28ce0392ead 100644 --- a/src/eval/test/rc_conformance_tests.zig +++ b/src/eval/test/rc_conformance_tests.zig @@ -721,7 +721,7 @@ const cases = [_]Case{ \\ shared_kept = List.keep_if(shared, |text| text.count_utf8_bytes() > 33) \\ shared_all = List.keep_if(shared, |_| True) \\ shared_none = List.keep_if(shared, |_| False) - \\ oks =List.keep_oks(shared, |text| if text.count_utf8_bytes() > 33 { Ok(text) } else { Err(text) }) + \\ oks = List.keep_oks(shared, |text| if text.count_utf8_bytes() > 33 { Ok(text) } else { Err(text) }) \\ errs = List.keep_errs( \\ List.concat( \\ ["a list element long enough to allocate", "another list element long enough"], diff --git a/test/alloc-count/app_list_keep_if.roc b/test/alloc-count/app_list_keep_if.roc index c28fd6fd652..69e4365a991 100644 --- a/test/alloc-count/app_list_keep_if.roc +++ b/test/alloc-count/app_list_keep_if.roc @@ -4,8 +4,8 @@ import pf.Host # List.keep_if and List.drop_if compact a uniquely owned list in place, so # they allocate nothing. A shared list, and keep_oks, whose item type -# changes, build their result in one allocation, and allocate nothing when -# they keep nothing. A shared list that keeps every item comes back as it is. +# changes, build their result in one allocation. keep_oks allocates nothing +# when it keeps nothing. A shared list that keeps every item comes back as it is. numbers : U64 -> List(U64) numbers = |offset| { var $list = List.with_capacity(8) diff --git a/test/snapshots/bang_on_numeric_literal.md b/test/snapshots/bang_on_numeric_literal.md index 4c6b1a1039f..5592673fd29 100644 --- a/test/snapshots/bang_on_numeric_literal.md +++ b/test/snapshots/bang_on_numeric_literal.md @@ -46,7 +46,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call (constraint-fn-var 222) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-runtime-error (tag "erroneous_value_expr"))) ~~~ # TYPES diff --git a/test/snapshots/bool_closure_type_check.md b/test/snapshots/bool_closure_type_check.md index cb7c9192e64..8cabdef46ca 100644 --- a/test/snapshots/bool_closure_type_check.md +++ b/test/snapshots/bool_closure_type_check.md @@ -38,7 +38,7 @@ NO CHANGE (args (p-assign (ident "x"))) (e-call (constraint-fn-var 219) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-lookup-local (p-assign (ident "x"))))) (e-tag (name "True"))) diff --git a/test/snapshots/dev_object_float_math.md b/test/snapshots/dev_object_float_math.md index 4fd368a2c6a..327a4c897ad 100644 --- a/test/snapshots/dev_object_float_math.md +++ b/test/snapshots/dev_object_float_math.md @@ -51,37 +51,37 @@ main = |x, y, n| { ~~~ # DEV OUTPUT ~~~ini -x64mac=0a6a8023cf9e6481c40547844ef5868916c50647b9cf83e427d42c27bb1a78aa -x64win=e552bb9aed9b18bc9ff852cd054a12cacef51e0395ffb988d84526ad058eaeed -x64mingw=e552bb9aed9b18bc9ff852cd054a12cacef51e0395ffb988d84526ad058eaeed -x64freebsd=28b28ae5b26fc1d309236b75febd0a87744a83df9a72ccabb06c9c6c4fb775cd -x64openbsd=877ea46139cc449b654c88b549662efe8ce1c56ccb94b997ca5ae494603d7faa -x64netbsd=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64musl=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64glibc=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64linux=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64elf=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64v1mac=0a6a8023cf9e6481c40547844ef5868916c50647b9cf83e427d42c27bb1a78aa -x64v1win=e552bb9aed9b18bc9ff852cd054a12cacef51e0395ffb988d84526ad058eaeed -x64v1mingw=e552bb9aed9b18bc9ff852cd054a12cacef51e0395ffb988d84526ad058eaeed -x64v1freebsd=28b28ae5b26fc1d309236b75febd0a87744a83df9a72ccabb06c9c6c4fb775cd -x64v1openbsd=877ea46139cc449b654c88b549662efe8ce1c56ccb94b997ca5ae494603d7faa -x64v1netbsd=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64v1musl=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64v1glibc=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64v1linux=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -x64v1elf=fa58d04888df47f4116c55a10efbacc5a1eb50113804dfbcd7ee3c71ec04b05f -arm64mac=4a3b95d88385254511c87faec3e78f9f5db70ab6586ad11caba8c604df09e9a7 -arm64win=9466e5f20613a29d6aa31e9afb3a10996b008b1419a037c40154afaeae004173 -arm64mingw=9466e5f20613a29d6aa31e9afb3a10996b008b1419a037c40154afaeae004173 -arm64linux=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 -arm64musl=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 -arm64glibc=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 -arm64v1win=9466e5f20613a29d6aa31e9afb3a10996b008b1419a037c40154afaeae004173 -arm64v1mingw=9466e5f20613a29d6aa31e9afb3a10996b008b1419a037c40154afaeae004173 -arm64v1linux=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 -arm64v1musl=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 -arm64v1glibc=201e3981c853167800866614f894c1325dbc9b584609388543b7914e95df5bd0 +x64mac=39c62a7538c70afb6a9b7d56af6e42f260210bfab53cb6d9b25fed2469aaba9d +x64win=a00aad72e34a993f7d9ac374ed9e4e6730394daf1996507f786e1d10092edf8d +x64mingw=a00aad72e34a993f7d9ac374ed9e4e6730394daf1996507f786e1d10092edf8d +x64freebsd=d9347d8c38f967d3da2858fa1c66a9bb0bc02df4bb95d6d23c389a38d476537d +x64openbsd=26a507fc8d1393f74cc1b3b60e1e82868fb9aa09d13d1f746058c7eeefba472e +x64netbsd=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64musl=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64glibc=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64linux=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64elf=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64v1mac=39c62a7538c70afb6a9b7d56af6e42f260210bfab53cb6d9b25fed2469aaba9d +x64v1win=a00aad72e34a993f7d9ac374ed9e4e6730394daf1996507f786e1d10092edf8d +x64v1mingw=a00aad72e34a993f7d9ac374ed9e4e6730394daf1996507f786e1d10092edf8d +x64v1freebsd=d9347d8c38f967d3da2858fa1c66a9bb0bc02df4bb95d6d23c389a38d476537d +x64v1openbsd=26a507fc8d1393f74cc1b3b60e1e82868fb9aa09d13d1f746058c7eeefba472e +x64v1netbsd=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64v1musl=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64v1glibc=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64v1linux=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +x64v1elf=3a99c730844e11642c2d2032fa687e0c6c919ff76a2bbe9ae13f7f3c5924a685 +arm64mac=241c759ecc89e661fad3bfaf5789e5ccc7e9eec307ae637f0ae30fcf91fece7b +arm64win=1e0fd2527f76f5937b7686e0b40b66615be40f3f6b2a129cf11ff1d0848fa4a0 +arm64mingw=1e0fd2527f76f5937b7686e0b40b66615be40f3f6b2a129cf11ff1d0848fa4a0 +arm64linux=98e8b0b717c7235f4edaf2c14b7a4feed2751213da2a7840063127cbc6c8527f +arm64musl=98e8b0b717c7235f4edaf2c14b7a4feed2751213da2a7840063127cbc6c8527f +arm64glibc=98e8b0b717c7235f4edaf2c14b7a4feed2751213da2a7840063127cbc6c8527f +arm64v1win=1e0fd2527f76f5937b7686e0b40b66615be40f3f6b2a129cf11ff1d0848fa4a0 +arm64v1mingw=1e0fd2527f76f5937b7686e0b40b66615be40f3f6b2a129cf11ff1d0848fa4a0 +arm64v1linux=98e8b0b717c7235f4edaf2c14b7a4feed2751213da2a7840063127cbc6c8527f +arm64v1musl=98e8b0b717c7235f4edaf2c14b7a4feed2751213da2a7840063127cbc6c8527f +arm64v1glibc=98e8b0b717c7235f4edaf2c14b7a4feed2751213da2a7840063127cbc6c8527f arm32linux=NOT_IMPLEMENTED arm32musl=NOT_IMPLEMENTED wasm32=NOT_IMPLEMENTED diff --git a/test/snapshots/ellipsis_expression_positions.md b/test/snapshots/ellipsis_expression_positions.md index 6a57d548262..d3d9a6e960c 100644 --- a/test/snapshots/ellipsis_expression_positions.md +++ b/test/snapshots/ellipsis_expression_positions.md @@ -1135,7 +1135,7 @@ lambda_arg = |f| f(|_| ...) (args (p-underscore)) (e-call (constraint-fn-var 658) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-not-implemented)))) (d-let (p-assign (ident "question")) diff --git a/test/snapshots/eval/tuple_bool.md b/test/snapshots/eval/tuple_bool.md index 6a4e1fb280c..51265d03548 100644 --- a/test/snapshots/eval/tuple_bool.md +++ b/test/snapshots/eval/tuple_bool.md @@ -53,10 +53,10 @@ NO CHANGE (builtin) (e-tag (name "False"))) (e-call (constraint-fn-var 257) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-tag (name "True"))) (e-call (constraint-fn-var 262) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-tag (name "False"))) (e-if (if-branches @@ -71,14 +71,14 @@ NO CHANGE (if-branches (if-branch (e-call (constraint-fn-var 279) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-tag (name "True"))) (e-nominal-external (builtin) (e-tag (name "True"))))) (if-else (e-call (constraint-fn-var 290) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-tag (name "True"))))))) ~~~ # TYPES diff --git a/test/snapshots/expr/not_tag.md b/test/snapshots/expr/not_tag.md index a1a891b1cec..cbdd5696603 100644 --- a/test/snapshots/expr/not_tag.md +++ b/test/snapshots/expr/not_tag.md @@ -66,7 +66,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-tag (name "C") (args (e-num (value "2"))))) diff --git a/test/snapshots/expr/unary_not.md b/test/snapshots/expr/unary_not.md index ac9e23dbc5f..b16b5fea10b 100644 --- a/test/snapshots/expr/unary_not.md +++ b/test/snapshots/expr/unary_not.md @@ -28,7 +28,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES diff --git a/test/snapshots/expr/unary_op_not.md b/test/snapshots/expr/unary_op_not.md index f83289c1348..c91fee24803 100644 --- a/test/snapshots/expr/unary_op_not.md +++ b/test/snapshots/expr/unary_op_not.md @@ -28,7 +28,7 @@ NO CHANGE # CANONICALIZE ~~~clojure (e-call - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES diff --git a/test/snapshots/multiline_string_complex.md b/test/snapshots/multiline_string_complex.md index eb68de36e0b..f33d082ae55 100644 --- a/test/snapshots/multiline_string_complex.md +++ b/test/snapshots/multiline_string_complex.md @@ -355,7 +355,7 @@ With multiple lines (e-string))) (field (name "e") (e-call (constraint-fn-var 372) - (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18412") (target-def "18412")) + (e-lookup-associated-resolved (source "Bool.not") (builtin) (target-node "18404") (target-def "18404")) (e-runtime-error (tag "erroneous_value_expr"))))))) (d-let (p-assign (ident "x"))