diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index e5872f54e36..d69a32bd9bf 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -531,6 +531,7 @@ Builtin :: [].{ rest: Iter.custom( next_seed, match len_if_known { + Known(0) => Unknown Known(l) => Known(l - 1) Unknown => Unknown }, @@ -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 @@ -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 } } @@ -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 @@ -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 } } diff --git a/src/cli/main.zig b/src/cli/main.zig index ca41447c39d..a044f585767 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -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, @@ -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, @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); diff --git a/src/eval/test/eval_tests.zig b/src/eval/test/eval_tests.zig index de7c94818d3..91743b73783 100644 --- a/src/eval/test/eval_tests.zig +++ b/src/eval/test/eval_tests.zig @@ -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 = diff --git a/src/eval/test/lir_inline_test.zig b/src/eval/test/lir_inline_test.zig index 5fad3833413..c3ba0410adb 100644 --- a/src/eval/test/lir_inline_test.zig +++ b/src/eval/test/lir_inline_test.zig @@ -741,14 +741,11 @@ 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 @@ -756,10 +753,10 @@ fn procShapeMatchesIterCollect(shape: ProcShape, wanted: IterCollectShape) bool 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, }; }