From 63f357599818e1eb6a97748a300331533cbb34e6 Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Mon, 22 Apr 2024 09:25:14 -0500 Subject: [PATCH 1/6] make `IteratorSize(::Iterators.Cycle)` not always `IsInfinite` and document why --- base/generator.jl | 12 ++++++++++++ base/iterators.jl | 8 +++++++- test/iterators.jl | 12 ++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/base/generator.jl b/base/generator.jl index 1f981de8dc788..4f2556813cfad 100644 --- a/base/generator.jl +++ b/base/generator.jl @@ -83,12 +83,24 @@ This means that most iterators are assumed to implement [`length`](@ref). This trait is generally used to select between algorithms that pre-allocate space for their result, and algorithms that resize their result incrementally. +Most iterators' `IteratorSize` category can be determined from their type. In this case +the generic `IteratorSize(iterator) = IteratorSize(typeof(iterator))` fallback applies. +However, some iterators (e.g. [`Iterators.cycle`](@ref)) have a size category that can only +be determined at runtime. In this case the `IteratorSize(itr)` method will not be type +stable and the `IteratorSize(::Type)` method will return `SizeUnkown`. + ```jldoctest julia> Base.IteratorSize(1:5) Base.HasShape{1}() julia> Base.IteratorSize((2,3)) Base.HasLength() + +julia> Base.IteratorSize(Iterators.cycle(1:5)) +Base.IsInfinite() + +julia> Base.IteratorSize(Iterators.cycle(1:0)) +Base.HasLength() ``` """ IteratorSize(x) = IteratorSize(typeof(x)) diff --git a/base/iterators.jl b/base/iterators.jl index a0b3a6cd4672d..5dd34c18aae72 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -988,7 +988,13 @@ cycle(xs, n::Integer) = flatten(repeated(xs, n)) eltype(::Type{Cycle{I}}) where {I} = eltype(I) IteratorEltype(::Type{Cycle{I}}) where {I} = IteratorEltype(I) -IteratorSize(::Type{Cycle{I}}) where {I} = IsInfinite() # XXX: this is false if iterator ever becomes empty +function IteratorSize(::Type{Cycle{I}}) where I + # TODO: find a better way of communicating the size of a cycle + # IsInfinite() would be false if iterator ever becomes empty + IteratorSize(I) === IsInfinite() ? IsInfinite() : SizeUnknown() +end +IteratorSize(it::Cycle) = isempty(it.xs) ? HasLength() : IsInfinite() +length(it::Cycle) = isempty(it.xs) ? 0 : throw(ArgumentError("Cannot compute length of infinite iterator")) iterate(it::Cycle) = iterate(it.xs) isdone(it::Cycle) = isdone(it.xs) diff --git a/test/iterators.jl b/test/iterators.jl index 3616a17b31d31..e7ae8f0002bae 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1049,3 +1049,15 @@ end @testset "Iterators docstrings" begin @test isempty(Docs.undocumented_names(Iterators)) end + +@testset "cycle IteratorSize (#53169)" begin + @test Base.IteratorSize(Iterators.cycle(1:3)) == Base.IsInfinite() + @test Base.IteratorSize(Iterators.cycle(1:0)) == Base.HasLength() + @test length(Iterators.cycle(1:0)) == 0 + @test collect(Iterators.cycle(1:0))::Vector{Int} == Int[] + @test Base.IteratorSize(Iterators.cycle(Iterators.Repeated(6))) == Base.IsInfinite() + + @test Base.IteratorSize(typeof(Iterators.cycle(1:3))) == Base.SizeUnknown() + @test Base.IteratorSize(typeof(Iterators.cycle(1:0))) == Base.SizeUnknown() + @test Base.IteratorSize(typeof(Iterators.cycle(Iterators.Repeated(6)))) == Base.IsInfinite() +end From 7b0eba0faf59ca0d7333865773dc645ba1c9b2e0 Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Mon, 22 Apr 2024 15:13:03 +0000 Subject: [PATCH 2/6] Fix typo Co-authored-by: adienes <51664769+adienes@users.noreply.github.com> --- base/generator.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/generator.jl b/base/generator.jl index 4f2556813cfad..9feecb727c075 100644 --- a/base/generator.jl +++ b/base/generator.jl @@ -87,7 +87,7 @@ Most iterators' `IteratorSize` category can be determined from their type. In th the generic `IteratorSize(iterator) = IteratorSize(typeof(iterator))` fallback applies. However, some iterators (e.g. [`Iterators.cycle`](@ref)) have a size category that can only be determined at runtime. In this case the `IteratorSize(itr)` method will not be type -stable and the `IteratorSize(::Type)` method will return `SizeUnkown`. +stable and the `IteratorSize(::Type)` method will return `SizeUnknown`. ```jldoctest julia> Base.IteratorSize(1:5) From f7bf6f141664c947d071479a9fd2781656489340 Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Thu, 25 Sep 2025 17:53:45 -0500 Subject: [PATCH 3/6] Apply triage's suggestion --- base/generator.jl | 6 ------ base/iterators.jl | 6 +----- test/iterators.jl | 5 ++--- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/base/generator.jl b/base/generator.jl index 9feecb727c075..aedbbb66c3151 100644 --- a/base/generator.jl +++ b/base/generator.jl @@ -83,12 +83,6 @@ This means that most iterators are assumed to implement [`length`](@ref). This trait is generally used to select between algorithms that pre-allocate space for their result, and algorithms that resize their result incrementally. -Most iterators' `IteratorSize` category can be determined from their type. In this case -the generic `IteratorSize(iterator) = IteratorSize(typeof(iterator))` fallback applies. -However, some iterators (e.g. [`Iterators.cycle`](@ref)) have a size category that can only -be determined at runtime. In this case the `IteratorSize(itr)` method will not be type -stable and the `IteratorSize(::Type)` method will return `SizeUnknown`. - ```jldoctest julia> Base.IteratorSize(1:5) Base.HasShape{1}() diff --git a/base/iterators.jl b/base/iterators.jl index 5dd34c18aae72..142dd9252ef72 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -988,13 +988,9 @@ cycle(xs, n::Integer) = flatten(repeated(xs, n)) eltype(::Type{Cycle{I}}) where {I} = eltype(I) IteratorEltype(::Type{Cycle{I}}) where {I} = IteratorEltype(I) -function IteratorSize(::Type{Cycle{I}}) where I - # TODO: find a better way of communicating the size of a cycle - # IsInfinite() would be false if iterator ever becomes empty +function IteratorSize(::Type{Cycle{I}}) where {I} IteratorSize(I) === IsInfinite() ? IsInfinite() : SizeUnknown() end -IteratorSize(it::Cycle) = isempty(it.xs) ? HasLength() : IsInfinite() -length(it::Cycle) = isempty(it.xs) ? 0 : throw(ArgumentError("Cannot compute length of infinite iterator")) iterate(it::Cycle) = iterate(it.xs) isdone(it::Cycle) = isdone(it.xs) diff --git a/test/iterators.jl b/test/iterators.jl index e7ae8f0002bae..2eee74aef9a46 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1051,9 +1051,8 @@ end end @testset "cycle IteratorSize (#53169)" begin - @test Base.IteratorSize(Iterators.cycle(1:3)) == Base.IsInfinite() - @test Base.IteratorSize(Iterators.cycle(1:0)) == Base.HasLength() - @test length(Iterators.cycle(1:0)) == 0 + @test Base.IteratorSize(Iterators.cycle(1:3)) == Base.SizeUnknown() + @test Base.IteratorSize(Iterators.cycle(1:0)) == Base.SizeUnknown() @test collect(Iterators.cycle(1:0))::Vector{Int} == Int[] @test Base.IteratorSize(Iterators.cycle(Iterators.Repeated(6))) == Base.IsInfinite() From 42b6c7ba67cf5f372e4c583d91673e7d50bd8612 Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Thu, 25 Sep 2025 17:54:58 -0500 Subject: [PATCH 4/6] Fixup jldoctest --- base/generator.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/generator.jl b/base/generator.jl index aedbbb66c3151..f5816b3cd9dc2 100644 --- a/base/generator.jl +++ b/base/generator.jl @@ -91,10 +91,10 @@ julia> Base.IteratorSize((2,3)) Base.HasLength() julia> Base.IteratorSize(Iterators.cycle(1:5)) -Base.IsInfinite() +Base.SizeUnknown() julia> Base.IteratorSize(Iterators.cycle(1:0)) -Base.HasLength() +Base.SizeUnknown() ``` """ IteratorSize(x) = IteratorSize(typeof(x)) From 7c19b7fb99f036b9f9a488bea1553d074079e68e Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Thu, 25 Sep 2025 17:56:23 -0500 Subject: [PATCH 5/6] Know that `HasShape{0}()` is not empty --- base/iterators.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/base/iterators.jl b/base/iterators.jl index 142dd9252ef72..ebef8a92c1bce 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -989,7 +989,8 @@ cycle(xs, n::Integer) = flatten(repeated(xs, n)) eltype(::Type{Cycle{I}}) where {I} = eltype(I) IteratorEltype(::Type{Cycle{I}}) where {I} = IteratorEltype(I) function IteratorSize(::Type{Cycle{I}}) where {I} - IteratorSize(I) === IsInfinite() ? IsInfinite() : SizeUnknown() + IS = IteratorSize(I) + (IS === IsInfinite() || IS == HasShape{0}()) ? IsInfinite() : SizeUnknown() end iterate(it::Cycle) = iterate(it.xs) From eaad6758bd353b60da468e94c3a40005b46e9cb4 Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Thu, 25 Sep 2025 17:57:11 -0500 Subject: [PATCH 6/6] Test that HasShape --- test/iterators.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/iterators.jl b/test/iterators.jl index 2eee74aef9a46..f6bc3d0267e4c 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1053,6 +1053,7 @@ end @testset "cycle IteratorSize (#53169)" begin @test Base.IteratorSize(Iterators.cycle(1:3)) == Base.SizeUnknown() @test Base.IteratorSize(Iterators.cycle(1:0)) == Base.SizeUnknown() + @test Base.IteratorSize(Iterators.cycle(7)) == Base.IsInfinite() @test collect(Iterators.cycle(1:0))::Vector{Int} == Int[] @test Base.IteratorSize(Iterators.cycle(Iterators.Repeated(6))) == Base.IsInfinite()