Skip to content
Open
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
28 changes: 11 additions & 17 deletions src/build/roc/Builtin.roc
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ Builtin :: [].{
rest: Iter.custom(
next_seed,
match len_if_known {
Known(0) => Unknown
Known(l) => Known(l - 1)
Unknown => Unknown
},
Expand Down Expand Up @@ -1016,10 +1017,10 @@ Builtin :: [].{
## into a [List] (pre-sized from `len_if_known` when known).
collect! : Stream(item) => List(item)
collect! = |stream| {
# `Known(n)` guarantees exactly n items (count-changing combinators
# report `Unknown`), so reserve up front and use the unchecked append.
# When the length is unknown, start empty and grow with the reserving
# append — the unchecked append would corrupt a zero-capacity list.
# `len_if_known` is a size hint exposed through public iterator
# constructors. Reserve from it when present, but use the reserving
# append so an over-producing custom iterator cannot write past the
# hinted capacity.
length = Stream.size_hint(stream)
cap = match length {
Known(n) => n
Expand All @@ -1036,10 +1037,7 @@ Builtin :: [].{
$rest = rest
}
One({ item, rest }) => {
$list = match length {
Known(_) => list_append_unsafe($list, item)
Unknown => List.append($list, item)
}
$list = List.append($list, item)
$rest = rest
}
}
Expand Down Expand Up @@ -1090,11 +1088,10 @@ Builtin :: [].{
## that [Iter.collect] dispatches to.
from_iter : Iter(item) -> List(item)
from_iter = |iterator| {
# `Known(n)` guarantees the iterator yields exactly n items: every
# combinator that can change the count (e.g. keep_if) reports `Unknown`.
# So when the length is known we reserve the whole allocation up front
# and write each item with the unchecked append; when it is unknown we
# start empty and grow with the reserving append instead.
# `len_if_known` is a size hint exposed through public iterator
# constructors. Reserve from it when present, but use the reserving
# append so an over-producing custom iterator cannot write past the
# hinted capacity.
length = iterator.len_if_known
cap = match length {
Known(n) => n
Expand All @@ -1113,10 +1110,7 @@ Builtin :: [].{
$rest = rest
}
One({ item, rest }) => {
$list = match length {
Known(_) => list_append_unsafe($list, item)
Unknown => List.append($list, item)
}
$list = List.append($list, item)
$rest = rest
}
}
Expand Down
34 changes: 14 additions & 20 deletions src/cli/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4511,12 +4511,6 @@ fn testingSharedMemoryHandle(shm: *SharedMemoryAllocator) SharedMemoryHandle {
};
}

fn testingHotReloadDescriptor(shm: *SharedMemoryAllocator, offset: usize) *ipc.hot_reload.ImageDescriptor {
// Route through the production helper so tests commit the descriptor's page on
// Windows (SEC_RESERVE leaves it reserved-but-uncommitted) exactly as a real run does.
return hotReloadDescriptorForWrite(shm, offset) catch unreachable;
}

fn testingPrepareHotReloadDescriptor(
shm: *SharedMemoryAllocator,
generation: u64,
Expand All @@ -4525,8 +4519,8 @@ fn testingPrepareHotReloadDescriptor(
image_bound: usize,
allocation_start: usize,
allocation_end: usize,
) *ipc.hot_reload.ImageDescriptor {
const descriptor = testingHotReloadDescriptor(shm, descriptor_offset);
) CliMainError!*ipc.hot_reload.ImageDescriptor {
const descriptor = try hotReloadDescriptorForWrite(shm, descriptor_offset);
ipc.hot_reload.prepareDescriptor(
descriptor,
generation,
Expand Down Expand Up @@ -4560,15 +4554,15 @@ test "hot reload allocation coalesces adjacent reclaimed image regions" {
const desc2_offset = hotReloadPreviousDescriptorOffset(desc1_offset).?;

const control = ipc.hot_reload.controlFromBase(shm.base_ptr);
const desc0 = testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 512, 2048, 512, 2048);
const desc0 = try testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 512, 2048, 512, 2048);
ipc.hot_reload.init(control, desc0_offset, desc0);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 2048);

const desc1 = testingPrepareHotReloadDescriptor(&shm, 2, desc1_offset, 2048, 4096, 2048, 4096);
const desc1 = try testingPrepareHotReloadDescriptor(&shm, 2, desc1_offset, 2048, 4096, 2048, 4096);
ipc.hot_reload.publishDescriptor(control, 2, desc1_offset, desc1);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 4096);

const desc2 = testingPrepareHotReloadDescriptor(&shm, 3, desc2_offset, 4096, 8192, 4096, 8192);
const desc2 = try testingPrepareHotReloadDescriptor(&shm, 3, desc2_offset, 4096, 8192, 4096, 8192);
ipc.hot_reload.publishDescriptor(control, 3, desc2_offset, desc2);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 8192);

Expand Down Expand Up @@ -4603,11 +4597,11 @@ test "hot reload sweep keeps acknowledged current descriptor live" {
const desc1_offset = hotReloadPreviousDescriptorOffset(desc0_offset).?;

const control = ipc.hot_reload.controlFromBase(shm.base_ptr);
const desc0 = testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 512, 2048, 512, 2048);
const desc0 = try testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 512, 2048, 512, 2048);
ipc.hot_reload.init(control, desc0_offset, desc0);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 2048);

const desc1 = testingPrepareHotReloadDescriptor(&shm, 2, desc1_offset, 2048, 4096, 2048, 4096);
const desc1 = try testingPrepareHotReloadDescriptor(&shm, 2, desc1_offset, 2048, 4096, 2048, 4096);
ipc.hot_reload.publishDescriptor(control, 2, desc1_offset, desc1);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 4096);
ipc.hot_reload.acknowledge(control, 2, .accepted);
Expand Down Expand Up @@ -4640,11 +4634,11 @@ test "hot reload sweep keeps top reclaimed descriptor region addressable" {
const desc1_offset = hotReloadPreviousDescriptorOffset(desc0_offset).?;

const control = ipc.hot_reload.controlFromBase(shm.base_ptr);
const desc0 = testingPrepareHotReloadDescriptor(&shm, 3, desc0_offset, 512, 2048, 512, 2048);
const desc0 = try testingPrepareHotReloadDescriptor(&shm, 3, desc0_offset, 512, 2048, 512, 2048);
ipc.hot_reload.init(control, desc0_offset, desc0);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 2048);

const desc1 = testingPrepareHotReloadDescriptor(&shm, 2, desc1_offset, 2048, 4096, 2048, 4096);
const desc1 = try testingPrepareHotReloadDescriptor(&shm, 2, desc1_offset, 2048, 4096, 2048, 4096);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 4096);
ipc.hot_reload.publishDescriptor(control, 3, desc0_offset, desc0);

Expand Down Expand Up @@ -4678,17 +4672,17 @@ test "hot reload allocation skips retired retained descriptors" {
const desc2_offset = hotReloadPreviousDescriptorOffset(desc1_offset).?;

const control = ipc.hot_reload.controlFromBase(shm.base_ptr);
const desc0 = testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 512, 2048, 512, 2048);
const desc0 = try testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 512, 2048, 512, 2048);
ipc.hot_reload.init(control, desc0_offset, desc0);
ipc.hot_reload.retainDescriptor(desc0);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 2048);

const desc1 = testingPrepareHotReloadDescriptor(&shm, 2, desc1_offset, 2048, 4096, 2048, 4096);
const desc1 = try testingPrepareHotReloadDescriptor(&shm, 2, desc1_offset, 2048, 4096, 2048, 4096);
ipc.hot_reload.publishDescriptor(control, 2, desc1_offset, desc1);
ipc.hot_reload.retainDescriptor(desc1);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 4096);

const desc2 = testingPrepareHotReloadDescriptor(&shm, 3, desc2_offset, 4096, 8192, 4096, 8192);
const desc2 = try testingPrepareHotReloadDescriptor(&shm, 3, desc2_offset, 4096, 8192, 4096, 8192);
ipc.hot_reload.publishDescriptor(control, 3, desc2_offset, desc2);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, 8192);

Expand Down Expand Up @@ -4724,7 +4718,7 @@ test "hot reload allocation rolls back fresh descriptor when append has no room"
const desc1_offset = hotReloadPreviousDescriptorOffset(desc0_offset).?;

const control = ipc.hot_reload.controlFromBase(shm.base_ptr);
const desc0 = testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 512, desc1_offset, 512, desc1_offset);
const desc0 = try testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 512, desc1_offset, 512, desc1_offset);
ipc.hot_reload.init(control, desc0_offset, desc0);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, desc1_offset);

Expand Down Expand Up @@ -4752,7 +4746,7 @@ test "hot reload allocation can use reclaimed region when append has no room" {
const desc1_offset = hotReloadPreviousDescriptorOffset(desc0_offset).?;

const control = ipc.hot_reload.controlFromBase(shm.base_ptr);
const desc0 = testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 4096, desc1_offset, 4096, desc1_offset);
const desc0 = try testingPrepareHotReloadDescriptor(&shm, 1, desc0_offset, 4096, desc1_offset, 4096, desc1_offset);
ipc.hot_reload.init(control, desc0_offset, desc0);
try SharedMemoryAllocator.rewindMappedHeader(shm.base_ptr, shm.total_size, desc1_offset);

Expand Down
21 changes: 21 additions & 0 deletions src/eval/test/eval_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3647,6 +3647,27 @@ const core_tests = [_]TestCase{
,
.expected = .{ .inspect_str = "[0, 1, 1, 2, 3]" },
},
.{
.name = "inspect: Iter.collect handles custom iterator exceeding known hint",
.source_kind = .module,
.source =
\\main = {
\\ adv : (U64 -> Try((U64, U64), [NoMore]))
\\ adv = |n|
\\ if n < 9 {
\\ Try.Ok((n, n + 1))
\\ } else {
\\ Try.Err(NoMore)
\\ }
\\
\\ collected : List(U64)
\\ collected = Iter.collect(Iter.custom(0.U64, Known(8), adv))
\\
\\ collected
\\}
,
.expected = .{ .inspect_str = "[0, 1, 2, 3, 4, 5, 6, 7, 8]" },
},
.{
.name = "inspect: Iter.step_by yields first then every nth",
.source =
Expand Down
21 changes: 9 additions & 12 deletions src/eval/test/lir_inline_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -741,25 +741,22 @@ const IterCollectShape = enum {

fn procShapeMatchesIterCollect(shape: ProcShape, wanted: IterCollectShape) bool {
// Fingerprints of the `Iter.collect` -> `List.from_iter` worker over a range.
// `from_iter` branches on the iterator's length: a Known length reserves the
// whole allocation up front and writes each item with the unchecked append,
// while an Unknown length grows with the reserving append. That per-element
// branch (the inner `match length`) is the extra switch/join/jump over the
// earlier single-append loop. Spec constr specializes the worker for the
// concrete element type, and because ranges carry a Known length (via each
// numeric type's `steps_between`) the specialized worker threads that count
// as a third arg (`with_capacity` preallocation).
// `from_iter` uses the iterator's length only to pre-size the allocation,
// then writes each item through the reserving append. Spec constr specializes
// the worker for the concrete element type, and because ranges carry a Known
// length (via each numeric type's `steps_between`) the specialized worker
// threads that count as a third arg (`with_capacity` preallocation).
return switch (wanted) {
.specialized => shape.arg_count == 3 and
shape.direct_call_count >= 5 and
shape.switch_count >= 10 and
shape.join_count >= 16 and
shape.jump_count >= 20,
.generic => shape.arg_count == 1 and
shape.direct_call_count == 4 and
shape.switch_count == 8 and
shape.join_count == 12 and
shape.jump_count == 15 and
shape.direct_call_count == 3 and
shape.switch_count == 6 and
shape.join_count == 9 and
shape.jump_count == 11 and
shape.struct_assign_count >= 8,
};
}
Expand Down