-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Check that axes start with 1 for AbstractRange operations #30950
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -140,7 +140,20 @@ abstract type AbstractRange{T} <: AbstractArray{T,1} end | |
RangeStepStyle(::Type{<:AbstractRange}) = RangeStepIrregular() | ||
RangeStepStyle(::Type{<:AbstractRange{<:Integer}}) = RangeStepRegular() | ||
|
||
convert(::Type{T}, r::AbstractRange) where {T<:AbstractRange} = r isa T ? r : T(r) | ||
AxesStartStyle(::Type{<:AbstractRange}) = AxesStart1() # opt-out of AxesStart1 for "weird" range types | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In some ways I'd prefer "opt in" on this trait, but I think it's too breaking. So I think it's better to mark |
||
AxesStartStyle(r::AbstractRange) = AxesStartStyle(typeof(r)) | ||
|
||
convert(::Type{AbstractRange}, r::AbstractRange) = r | ||
convert(::Type{T}, r::T) where {T<:AbstractRange} = r | ||
convert(::Type{T}, r::AbstractRange) where {T<:AbstractRange} = _convert(AxesStartStyle(T), AxesStartStyle(r), T, r) | ||
_convert(::AxesStart1, ::AxesStart1, ::Type{T}, r::AbstractRange) where {T<:AbstractRange} = T(r) | ||
_convert(::AxesStartStyle, ::AxesStartStyle, ::Type{T}, r::AbstractRange) where {T<:AbstractRange} = | ||
throw(MethodError(convert, (T, r))) | ||
|
||
require_one_based_indexing(r::AbstractRange) = _require_one_based_indexing(AxesStartStyle(r), r) | ||
_require_one_based_indexing(::AxesStartStyle, r) = | ||
!has_offset_axes(r) || throw(ArgumentError("offset arrays are not supported but got an array with index other than 1")) | ||
_require_one_based_indexing(::AxesStart1, r) = true | ||
|
||
## ordinal ranges | ||
|
||
|
@@ -157,6 +170,8 @@ type can represent values smaller than `oneunit(Float64)`. | |
""" | ||
abstract type OrdinalRange{T,S} <: AbstractRange{T} end | ||
|
||
convert(::Type{OrdinalRange{T,S}}, r::OrdinalRange{T,S}) where {T,S} = r | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not strictly necessary for this PR, I think, but I noticed their lack and thought it might be good for disambiguating. |
||
|
||
""" | ||
AbstractUnitRange{T} <: OrdinalRange{T, T} | ||
|
||
|
@@ -165,6 +180,8 @@ Supertype for ranges with a step size of [`oneunit(T)`](@ref) with elements of t | |
""" | ||
abstract type AbstractUnitRange{T} <: OrdinalRange{T,T} end | ||
|
||
convert(::Type{AbstractUnitRange{T}}, r::AbstractUnitRange{T}) where {T} = r | ||
|
||
""" | ||
StepRange{T, S} <: OrdinalRange{T, S} | ||
|
||
|
@@ -307,15 +324,15 @@ be 1. | |
struct OneTo{T<:Integer} <: AbstractUnitRange{T} | ||
stop::T | ||
OneTo{T}(stop) where {T<:Integer} = new(max(zero(T), stop)) | ||
function OneTo{T}(r::AbstractRange) where {T<:Integer} | ||
throwstart(r) = (@_noinline_meta; throw(ArgumentError("first element must be 1, got $(first(r))"))) | ||
throwstep(r) = (@_noinline_meta; throw(ArgumentError("step must be 1, got $(step(r))"))) | ||
first(r) == 1 || throwstart(r) | ||
step(r) == 1 || throwstep(r) | ||
return new(max(zero(T), last(r))) | ||
end | ||
end | ||
OneTo(stop::T) where {T<:Integer} = OneTo{T}(stop) | ||
function OneTo{T}(r::AbstractRange) where {T<:Integer} | ||
throwstart(r) = (@_noinline_meta; throw(ArgumentError("first element must be 1, got $(first(r))"))) | ||
throwstep(r) = (@_noinline_meta; throw(ArgumentError("step must be 1, got $(step(r))"))) | ||
first(r) == 1 || throwstart(r) | ||
step(r) == 1 || throwstep(r) | ||
return OneTo{T}(last(r)) | ||
end | ||
OneTo(r::AbstractRange{T}) where {T<:Integer} = OneTo{T}(r) | ||
|
||
## Step ranges parameterized by length | ||
|
@@ -713,10 +730,14 @@ show(io::IO, r::AbstractRange) = print(io, repr(first(r)), ':', repr(step(r)), ' | |
show(io::IO, r::UnitRange) = print(io, repr(first(r)), ':', repr(last(r))) | ||
show(io::IO, r::OneTo) = print(io, "Base.OneTo(", r.stop, ")") | ||
|
||
range_axes_first_same(r, s) = _range_axes_first_same(AxesStartStyle(r), AxesStartStyle(s), r, s) | ||
_range_axes_first_same(::AxesStart1, ::AxesStart1, r, s) = true | ||
_range_axes_first_same(::AxesStartStyle, ::AxesStartStyle, r, s) = first(axes1(r)) == first(axes1(s)) | ||
|
||
==(r::T, s::T) where {T<:AbstractRange} = | ||
(first(r) == first(s)) & (step(r) == step(s)) & (last(r) == last(s)) | ||
(first(r) == first(s)) & (step(r) == step(s)) & (last(r) == last(s)) & range_axes_first_same(r, s) | ||
==(r::OrdinalRange, s::OrdinalRange) = | ||
(first(r) == first(s)) & (step(r) == step(s)) & (last(r) == last(s)) | ||
(first(r) == first(s)) & (step(r) == step(s)) & (last(r) == last(s)) & range_axes_first_same(r, s) | ||
==(r::T, s::T) where {T<:Union{StepRangeLen,LinRange}} = | ||
(first(r) == first(s)) & (length(r) == length(s)) & (last(r) == last(s)) | ||
==(r::Union{StepRange{T},StepRangeLen{T,T}}, s::Union{StepRange{T},StepRangeLen{T,T}}) where {T} = | ||
|
@@ -727,6 +748,7 @@ function ==(r::AbstractRange, s::AbstractRange) | |
if lr != length(s) | ||
return false | ||
end | ||
range_axes_first_same(r, s) || return false | ||
yr, ys = iterate(r), iterate(s) | ||
while yr !== nothing | ||
yr[1] == ys[1] || return false | ||
|
@@ -849,7 +871,7 @@ end | |
|
||
## linear operations on ranges ## | ||
|
||
-(r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r)) | ||
-(r::OrdinalRange) = (require_one_based_indexing(r); range(-first(r), step=-step(r), length=length(r))) | ||
-(r::StepRangeLen{T,R,S}) where {T,R,S} = | ||
StepRangeLen{T,R,S}(-r.ref, -r.step, length(r), r.offset) | ||
-(r::LinRange) = LinRange(-r.start, -r.stop, length(r)) | ||
|
@@ -861,6 +883,9 @@ el_same(::Type{T}, a::Type{<:AbstractArray{T,n}}, b::Type{<:AbstractArray{S,n}}) | |
el_same(::Type{T}, a::Type{<:AbstractArray{S,n}}, b::Type{<:AbstractArray{T,n}}) where {T,S,n} = b | ||
el_same(::Type, a, b) = promote_typejoin(a, b) | ||
|
||
# promote_rule and more constructors | ||
# Note: construction is distinct from conversion, convert should check require_one_based_indexing(r) | ||
# but construction should not. | ||
promote_rule(a::Type{UnitRange{T1}}, b::Type{UnitRange{T2}}) where {T1,T2} = | ||
el_same(promote_type(T1,T2), a, b) | ||
UnitRange{T}(r::UnitRange{T}) where {T<:Real} = r | ||
|
@@ -944,7 +969,10 @@ end | |
Array{T,1}(r::AbstractRange{T}) where {T} = vcat(r) | ||
collect(r::AbstractRange) = vcat(r) | ||
|
||
reverse(r::OrdinalRange) = (:)(last(r), -step(r), first(r)) | ||
function reverse(r::OrdinalRange) | ||
require_one_based_indexing(r) | ||
(:)(last(r), -step(r), first(r)) | ||
end | ||
function reverse(r::StepRangeLen) | ||
# If `r` is empty, `length(r) - r.offset + 1 will be nonpositive hence | ||
# invalid. As `reverse(r)` is also empty, any offset would work so we keep | ||
|
@@ -964,8 +992,11 @@ sort!(r::AbstractUnitRange) = r | |
|
||
sort(r::AbstractRange) = issorted(r) ? r : reverse(r) | ||
|
||
sortperm(r::AbstractUnitRange) = 1:length(r) | ||
sortperm(r::AbstractRange) = issorted(r) ? (1:1:length(r)) : (length(r):-1:1) | ||
sortperm(r::AbstractUnitRange) = (require_one_based_indexing(r); 1:length(r)) | ||
function sortperm(r::AbstractRange) | ||
require_one_based_indexing(r) | ||
issorted(r) ? (1:1:length(r)) : (length(r):-1:1) | ||
end | ||
|
||
function sum(r::AbstractRange{<:Real}) | ||
l = length(r) | ||
|
@@ -1004,6 +1035,7 @@ function _define_range_op(@nospecialize f) | |
r1l = length(r1) | ||
(r1l == length(r2) || | ||
throw(DimensionMismatch("argument dimensions must match"))) | ||
require_one_based_indexing(r1, r2) | ||
range($f(first(r1), first(r2)), step=$f(step(r1), step(r2)), length=r1l) | ||
end | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,8 @@ | |
|
||
# for reductions that expand 0 dims to 1 | ||
reduced_index(i::OneTo) = OneTo(1) | ||
reduced_index(i::Union{Slice, IdentityUnitRange}) = first(i):first(i) | ||
reduced_index(i::Slice) = Slice(first(i):first(i)) | ||
reduced_index(i::IdentityUnitRange) = IdentityUnitRange(first(i):first(i)) | ||
reduced_index(i::AbstractUnitRange) = | ||
throw(ArgumentError( | ||
""" | ||
|
@@ -43,33 +44,24 @@ function reduced_indices0(inds::Indices{N}, d::Int) where N | |
end | ||
end | ||
|
||
function reduced_indices(inds::Indices{N}, region) where N | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially started modifying this code to fix a problem but then transitioned to the changes above. Still, this seemed like old code in need of some cleanup. |
||
rinds = [inds...] | ||
function check_reduced_region(region, N) | ||
for i in region | ||
isa(i, Integer) || throw(ArgumentError("reduced dimension(s) must be integers")) | ||
d = Int(i) | ||
if d < 1 | ||
throw(ArgumentError("region dimension(s) must be ≥ 1, got $d")) | ||
elseif d <= N | ||
rinds[d] = reduced_index(rinds[d]) | ||
if i < 1 | ||
throw(ArgumentError("region dimension(s) must be ≥ 1, got $i")) | ||
end | ||
end | ||
tuple(rinds...)::typeof(inds) | ||
return nothing | ||
end | ||
|
||
function reduced_indices(inds::Indices{N}, region) where N | ||
check_reduced_region(region, N) | ||
ntuple(i->in(i, region) ? reduced_index(inds[i]) : inds[i], Val(N))::typeof(inds) | ||
end | ||
|
||
function reduced_indices0(inds::Indices{N}, region) where N | ||
rinds = [inds...] | ||
for i in region | ||
isa(i, Integer) || throw(ArgumentError("reduced dimension(s) must be integers")) | ||
d = Int(i) | ||
if d < 1 | ||
throw(ArgumentError("region dimension(s) must be ≥ 1, got $d")) | ||
elseif d <= N | ||
rind = rinds[d] | ||
rinds[d] = isempty(rind) ? rind : reduced_index(rind) | ||
end | ||
end | ||
tuple(rinds...)::typeof(inds) | ||
check_reduced_region(region, N) | ||
ntuple(i->in(i, region) ? (r = inds[i]; isempty(r) ? r : reduced_index(r)) : inds[i], Val(N))::typeof(inds) | ||
end | ||
|
||
###### Generic reduction functions ##### | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsure about these erroring methods. Here's an interesting and troubling case:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fascinating. So the larger question is: which axis should be used as the "basis" for the output and which axis is being "extruded" over that basis?
If we have any non-singleton dimension then it's a no-brainer: that's our output axis and all other singleton axes are discarded as they're being "extruded". We demand that the axes values match, so multiple non-singleton dimensions are by definition identical or an error. (We do promote the axis type, though, such that an OffsetArray that happens to be 1-based will still return a similarly non-offset OffsetArray for type stability.)
But when we have multiple singleton dimensions it's not clear. On master, we just use the last-provided axis for the given dimension, which isn't great. It's not really a one-based vs. offset thing — the same happens for combinations of offset arrays:
I think the one-based vs. offset axis thing is a red herring — we don't really know which is going to matter in the output. So I say we just find some consistent (symmetric) rule that can generalize to multiple singleton offset axes, too. Some possibilities:
OffsetArray(rand(1), -1) .+ OffsetArray(rand(1), -1)
would no longer be offset. So let's ax this candidate.axes(fill(0), 2)
returns, so scalars and zero-dimensional arrays could still broadcast with anything). It may be interesting to try out, but it could make for a really annoying trap where an expression mostly works until you end up with a one-vector instead of a two-vector.minimum
ormaximum
element1
(biasing the comparison to favor either axes below or above1
so there aren't ties). Perhaps the arc of the Julian universe bends towards 1-based arrays.1
(again, biased to favor either above/below). The reasoning here would be that someone chose to have that offset; they should be allowed to keep it.