Skip to content

Commit

Permalink
Merge pull request #15449 from JuliaLang/teh/reshapedarrays2
Browse files Browse the repository at this point in the history
ReshapedArrays, take2
  • Loading branch information
timholy committed Apr 20, 2016
2 parents c7643cd + 5f7e1cb commit 97f80c3
Show file tree
Hide file tree
Showing 26 changed files with 268 additions and 107 deletions.
61 changes: 22 additions & 39 deletions base/abstractarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,13 @@ linearindexing(::LinearIndexing, ::LinearIndexing) = LinearSlow()
Expr(:block, Expr(:meta, :inline), ex)
end

# check along a single dimension
checkbounds(::Type{Bool}, sz::Integer, i) = throw(ArgumentError("unable to check bounds for indices of type $(typeof(i))"))
checkbounds(::Type{Bool}, sz::Integer, i::Real) = 1 <= i <= sz
checkbounds(::Type{Bool}, sz::Integer, ::Colon) = true
function checkbounds(::Type{Bool}, sz::Integer, r::Range)
@_propagate_inbounds_meta
isempty(r) || (checkbounds(Bool, sz, minimum(r)) && checkbounds(Bool, sz, maximum(r)))
isempty(r) | (checkbounds(Bool, sz, first(r)) & checkbounds(Bool, sz, last(r)))
end
checkbounds(::Type{Bool}, sz::Integer, I::AbstractArray{Bool}) = length(I) == sz
function checkbounds(::Type{Bool}, sz::Integer, I::AbstractArray)
Expand All @@ -112,48 +113,38 @@ function checkbounds(::Type{Bool}, sz::Integer, I::AbstractArray)
end
b
end

# check all dimensions
function checkbounds{N,T}(::Type{Bool}, sz::NTuple{N,Integer}, I1::T, I...)
@_inline_meta
checkbounds(Bool, sz[1], I1) & checkbounds(Bool, tail(sz), I...)
end
checkbounds{T<:Integer}(::Type{Bool}, sz::Tuple{T}, I1) = (@_inline_meta; checkbounds(Bool, sz[1], I1))
checkbounds{N}(::Type{Bool}, sz::NTuple{N,Integer}, I1) = (@_inline_meta; checkbounds(Bool, prod(sz), I1))
checkbounds{N}(::Type{Bool}, sz::NTuple{N,Integer}) = (@_inline_meta; checkbounds(Bool, sz, 1)) # for a[]

checkbounds(::Type{Bool}, sz::Tuple{}, i) = (@_inline_meta; checkbounds(Bool, 1, i))
function checkbounds(::Type{Bool}, sz::Tuple{}, i, I...)
@_inline_meta
checkbounds(Bool, 1, i) & checkbounds(Bool, (), I...)
end
# Prevent allocation of a GC frame by hiding the BoundsError in a noinline function
throw_boundserror(A, I) = (@_noinline_meta; throw(BoundsError(A, I)))

# Don't define index types on checkbounds to make extending easier
checkbounds(A::AbstractArray, I...) = (@_inline_meta; _internal_checkbounds(A, I...))
# The internal function is named _internal_checkbounds since there had been a
# _checkbounds previously that meant something different.
_internal_checkbounds(A::AbstractArray) = true
_internal_checkbounds(A::AbstractArray, I::AbstractArray{Bool}) = size(A) == size(I) || throw_boundserror(A, I)
_internal_checkbounds(A::AbstractArray, I::AbstractVector{Bool}) = length(A) == length(I) || throw_boundserror(A, I)
_internal_checkbounds(A::AbstractArray, I) = (@_inline_meta; checkbounds(Bool, length(A), I) || throw_boundserror(A, I))
function _internal_checkbounds(A::AbstractMatrix, I, J)
@_inline_meta
(checkbounds(Bool, size(A,1), I) && checkbounds(Bool, size(A,2), J)) ||
throw_boundserror(A, (I, J))
end
function _internal_checkbounds(A::AbstractArray, I, J)
function _internal_checkbounds(A::AbstractArray, I1, I...)
# having I1 seems important for good codegen
@_inline_meta
(checkbounds(Bool, size(A,1), I) && checkbounds(Bool, trailingsize(A,Val{2}), J)) ||
throw_boundserror(A, (I, J))
end
@generated function _internal_checkbounds(A::AbstractArray, I...)
meta = Expr(:meta, :inline)
N = length(I)
Isplat = [:(I[$d]) for d=1:N]
error = :(throw_boundserror(A, tuple($(Isplat...))))
args = Expr[:(checkbounds(Bool, size(A,$dim), I[$dim]) || $error) for dim in 1:N-1]
push!(args, :(checkbounds(Bool, trailingsize(A,Val{$N}), I[$N]) || $error))
Expr(:block, meta, args...)
checkbounds(Bool, size(A), I1, I...) || throw_boundserror(A, (I1, I...))
end

## Bounds-checking without errors ##
function checkbounds(::Type{Bool}, sz::Dims, I...)
n = length(I)
for dim = 1:(n-1)
checkbounds(Bool, sz[dim], I[dim]) || return false
end
s = sz[n]
for i = n+1:length(sz)
s *= sz[i]
end
checkbounds(Bool, s, I[n])
end
# See also specializations in multidimensional

## Constructors ##

Expand All @@ -167,14 +158,6 @@ similar( a::AbstractArray, T::Type, dims::Integer...) = similar(a, T, dims)
similar( a::AbstractArray, T::Type, dims::DimsInteger) = Array(T, dims...)
similar( a::AbstractArray, T::Type, dims::Dims) = Array(T, dims)

function reshape(a::AbstractArray, dims::Dims)
if prod(dims) != length(a)
throw(ArgumentError("dimensions must be consistent with array size (expected $(length(a)), got $(prod(dims)))"))
end
copy!(similar(a, dims), a)
end
reshape(a::AbstractArray, dims::Int...) = reshape(a, dims)

## from general iterable to any array

function copy!(dest::AbstractArray, src)
Expand Down
2 changes: 1 addition & 1 deletion base/abstractarraymath.jl
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function repmat(a::AbstractVector, m::Int)
end

# Generalized repmat
function repeat{T}(A::Array{T};
function repeat{T}(A::AbstractArray{T};
inner::Array{Int} = ones(Int, ndims(A)),
outer::Array{Int} = ones(Int, ndims(A)))
ndims_in = ndims(A)
Expand Down
7 changes: 5 additions & 2 deletions base/array.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,11 @@ function reinterpret{T,S,N}(::Type{T}, a::Array{S}, dims::NTuple{N,Int})
ccall(:jl_reshape_array, Array{T,N}, (Any, Any, Any), Array{T,N}, a, dims)
end

reshape(a::Vector, dims::Tuple{Int}) = reshape_a(a, dims)
reshape{N}(a::Array, dims::NTuple{N,Int}) = reshape_a(a, dims)

# reshaping to same # of dimensions
function reshape{T,N}(a::Array{T,N}, dims::NTuple{N,Int})
function reshape_a{T,N}(a::Array{T,N}, dims::NTuple{N,Int})
if prod(dims) != length(a)
throw(DimensionMismatch("new dimensions $(dims) must be consistent with array size $(length(a))"))
end
Expand All @@ -108,7 +111,7 @@ function reshape{T,N}(a::Array{T,N}, dims::NTuple{N,Int})
end

# reshaping to different # of dimensions
function reshape{T,N}(a::Array{T}, dims::NTuple{N,Int})
function reshape_a{T,N}(a::Array{T}, dims::NTuple{N,Int})
if prod(dims) != length(a)
throw(DimensionMismatch("new dimensions $(dims) must be consistent with array size $(length(a))"))
end
Expand Down
5 changes: 4 additions & 1 deletion base/bitarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,11 @@ function copy!(dest::BitArray, src::Array)
return unsafe_copy!(dest, 1, src, 1, length(src))
end

reshape(B::BitVector, dims::Tuple{Int}) = reshape_ba(B, dims)
reshape(B::BitArray, dims::Tuple{Int}) = reshape_ba(B, dims)
reshape{N}(B::BitArray, dims::NTuple{N,Int}) = reshape_ba(B, dims)

function reshape{N}(B::BitArray, dims::NTuple{N,Int})
function reshape_ba{N}(B::BitArray, dims::NTuple{N,Int})
prod(dims) == length(B) ||
throw(DimensionMismatch("new dimensions $(dims) must be consistent with array size $(length(B))"))
dims == size(B) && return B
Expand Down
6 changes: 3 additions & 3 deletions base/complex.jl
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ big{T<:AbstractFloat,N}(A::AbstractArray{Complex{T},N}) = convert(AbstractArray{

promote_array_type{S<:Union{Complex, Real}, AT<:AbstractFloat}(F, ::Type{S}, ::Type{Complex{AT}}) = Complex{AT}

function complex{S<:Real,T<:Real}(A::Array{S}, B::Array{T})
function complex{S<:Real,T<:Real}(A::AbstractArray{S}, B::AbstractArray{T})
if size(A) != size(B); throw(DimensionMismatch()); end
F = similar(A, typeof(complex(zero(S),zero(T))))
for (iF, iA, iB) in zip(eachindex(F), eachindex(A), eachindex(B))
Expand All @@ -801,15 +801,15 @@ function complex{S<:Real,T<:Real}(A::Array{S}, B::Array{T})
return F
end

function complex{T<:Real}(A::Real, B::Array{T})
function complex{T<:Real}(A::Real, B::AbstractArray{T})
F = similar(B, typeof(complex(A,zero(T))))
for (iF, iB) in zip(eachindex(F), eachindex(B))
@inbounds F[iF] = complex(A, B[iB])
end
return F
end

function complex{T<:Real}(A::Array{T}, B::Real)
function complex{T<:Real}(A::AbstractArray{T}, B::Real)
F = similar(A, typeof(complex(zero(T),B)))
for (iF, iA) in zip(eachindex(F), eachindex(A))
@inbounds F[iF] = complex(A[iA], B)
Expand Down
12 changes: 6 additions & 6 deletions base/linalg/dense.jl
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ function trace{T}(A::Matrix{T})
t
end

function kron{T,S}(a::Matrix{T}, b::Matrix{S})
function kron{T,S}(a::AbstractMatrix{T}, b::AbstractMatrix{S})
R = Array(promote_type(T,S), size(a,1)*size(b,1), size(a,2)*size(b,2))
m = 1
for j = 1:size(a,2), l = 1:size(b,2), i = 1:size(a,1)
Expand All @@ -163,11 +163,11 @@ function kron{T,S}(a::Matrix{T}, b::Matrix{S})
R
end

kron(a::Number, b::Union{Number, Vector, Matrix}) = a * b
kron(a::Union{Vector, Matrix}, b::Number) = a * b
kron(a::Vector, b::Vector)=vec(kron(reshape(a,length(a),1),reshape(b,length(b),1)))
kron(a::Matrix, b::Vector)=kron(a,reshape(b,length(b),1))
kron(a::Vector, b::Matrix)=kron(reshape(a,length(a),1),b)
kron(a::Number, b::Union{Number, AbstractVecOrMat}) = a * b
kron(a::AbstractVecOrMat, b::Number) = a * b
kron(a::AbstractVector, b::AbstractVector)=vec(kron(reshape(a,length(a),1),reshape(b,length(b),1)))
kron(a::AbstractMatrix, b::AbstractVector)=kron(a,reshape(b,length(b),1))
kron(a::AbstractVector, b::AbstractMatrix)=kron(reshape(a,length(a),1),b)

^(A::Matrix, p::Integer) = p < 0 ? inv(A^-p) : Base.power_by_squaring(A,p)

Expand Down
4 changes: 2 additions & 2 deletions base/linalg/symmetric.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ convert{T}(::Type{AbstractMatrix{T}}, A::Symmetric) = Symmetric(convert(Abstract
convert{T,S<:AbstractMatrix}(::Type{Hermitian{T,S}},A::Hermitian{T,S}) = A
convert{T,S<:AbstractMatrix}(::Type{Hermitian{T,S}},A::Hermitian) = Hermitian{T,S}(convert(S,A.data),A.uplo)
convert{T}(::Type{AbstractMatrix{T}}, A::Hermitian) = Hermitian(convert(AbstractMatrix{T}, A.data), symbol(A.uplo))
copy{T,S}(A::Symmetric{T,S}) = Symmetric{T,S}(copy(A.data),A.uplo)
copy{T,S}(A::Hermitian{T,S}) = Hermitian{T,S}(copy(A.data),A.uplo)
copy{T,S}(A::Symmetric{T,S}) = (B = copy(A.data); Symmetric{T,typeof(B)}(B,A.uplo))
copy{T,S}(A::Hermitian{T,S}) = (B = copy(A.data); Hermitian{T,typeof(B)}(B,A.uplo))
ishermitian(A::Hermitian) = true
ishermitian{T<:Real,S}(A::Symmetric{T,S}) = true
ishermitian{T<:Complex,S}(A::Symmetric{T,S}) = all(imag(A.data) .== 0)
Expand Down
34 changes: 34 additions & 0 deletions base/multidimensional.jl
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,40 @@ end # IteratorsMD

using .IteratorsMD

# Bounds-checking specialization
# Specializing for a fixed number of arguments provides a ~25%
# improvement over the general definitions in abstractarray.jl
for N = 1:5
args = [:($(symbol(:I, d))) for d = 1:N]
targs = [:($(symbol(:I, d))::Union{Colon,Number,AbstractVector}) for d = 1:N] # prevent co-opting the CartesianIndex version
exs = [:(checkbounds(Bool, size(A, $d), $(args[d]))) for d = 1:N]
cbexpr = exs[1]
for d = 2:N
cbexpr = :($(exs[d]) & $cbexpr)
end
@eval begin
function checkbounds(A::AbstractArray, $(args...))
@_inline_meta
_internal_checkbounds(A, $(args...))
end
function _internal_checkbounds{T}(A::AbstractArray{T,$N}, $(targs...))
@_inline_meta
($cbexpr) || throw_boundserror(A, ($(args...),))
end
end
end

# Bounds-checking with CartesianIndex
@inline function checkbounds(::Type{Bool}, ::Tuple{}, I1::CartesianIndex)
checkbounds(Bool, (), I1.I...)
end
@inline function checkbounds(::Type{Bool}, sz::Tuple{}, I1::CartesianIndex, I...)
checkbounds(Bool, (), I1.I..., I...)
end
@inline function checkbounds(::Type{Bool}, sz::Dims, I1::CartesianIndex, I...)
checkbounds(Bool, sz, I1.I..., I...)
end

# Recursively compute the lengths of a list of indices, without dropping scalars
# These need to be inlined for more than 3 indexes
index_lengths(A::AbstractArray, I::Colon) = (length(A),)
Expand Down
2 changes: 1 addition & 1 deletion base/pointer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ cconvert(::Type{Ptr{UInt8}}, s::AbstractString) = bytestring(s)
cconvert(::Type{Ptr{Int8}}, s::AbstractString) = bytestring(s)

unsafe_convert{T}(::Type{Ptr{T}}, a::Array{T}) = ccall(:jl_array_ptr, Ptr{T}, (Any,), a)
unsafe_convert(::Type{Ptr{Void}}, a::Array) = ccall(:jl_array_ptr, Ptr{Void}, (Any,), a)
unsafe_convert{S,T}(::Type{Ptr{S}}, a::AbstractArray{T}) = convert(Ptr{S}, unsafe_convert(Ptr{T}, a))

# unsafe pointer to array conversions
function pointer_to_array{T}(p::Ptr{T}, d::Integer, own::Bool=false)
Expand Down
2 changes: 1 addition & 1 deletion base/reduce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ function extrema(A::AbstractArray, dims)
extrema!(B, A)
end

@generated function extrema!{T,N}(B, A::Array{T,N})
@generated function extrema!{T,N}(B, A::AbstractArray{T,N})
quote
sA = size(A)
sB = size(B)
Expand Down
107 changes: 107 additions & 0 deletions base/reshapedarray.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using Base.MultiplicativeInverses: SignedMultiplicativeInverse

immutable ReshapedArray{T,N,P<:AbstractArray,MI<:Tuple{Vararg{SignedMultiplicativeInverse{Int}}}} <: AbstractArray{T,N}
parent::P
dims::NTuple{N,Int}
mi::MI
end
ReshapedArray{T,N}(parent::AbstractArray{T}, dims::NTuple{N,Int}, mi) = ReshapedArray{T,N,typeof(parent),typeof(mi)}(parent, dims, mi)

# LinearFast ReshapedArray
typealias ReshapedArrayLF{T,N,P<:AbstractArray} ReshapedArray{T,N,P,Tuple{}}

# Fast iteration on ReshapedArrays: use the parent iterator
immutable ReshapedRange{I,M}
iter::I
mi::NTuple{M,SignedMultiplicativeInverse{Int}}
end
ReshapedRange(A::ReshapedArray) = reshapedrange(parent(A), A.mi)
function reshapedrange{M}(P, mi::NTuple{M})
iter = eachindex(P)
ReshapedRange{typeof(iter),M}(iter, mi)
end

immutable ReshapedIndex{T}
parentindex::T
end

# eachindex(A::ReshapedArray) = ReshapedRange(A) # TODO: uncomment this line
start(R::ReshapedRange) = start(R.iter)
@inline done(R::ReshapedRange, i) = done(R.iter, i)
@inline function next(R::ReshapedRange, i)
item, inext = next(R.iter, i)
ReshapedIndex(item), inext
end
length(R::ReshapedRange) = length(R.iter)

function reshape(parent::AbstractArray, dims::Dims)
prod(dims) == length(parent) || throw(DimensionMismatch("parent has $(length(parent)) elements, which is incompatible with size $dims"))
_reshape((parent, linearindexing(parent)), dims)
end
reshape(R::ReshapedArray, dims::Dims) = reshape(R.parent, dims)
reshape(a::AbstractArray, len::Int) = reshape(a, (len,))
reshape(a::AbstractArray, dims::Int...) = reshape(a, dims)

# When reshaping Vector->Vector, don't wrap with a ReshapedArray
reshape{T}(v::ReshapedArray{T,1}, dims::Tuple{Int}) = reshape(v.parent, dims[1])
reshape(v::AbstractVector, dims::Tuple{Int}) = reshape(v, dims[1])
function reshape(v::AbstractVector, len::Int)
len == length(v) || throw(DimensionMismatch("parent has $(length(v)) elements, which is incompatible with length $len"))
v
end

function _reshape(p::Tuple{AbstractArray,LinearSlow}, dims::Dims)
parent = p[1]
strds = front(size_strides(parent))
strds1 = map(s->max(1,s), strds) # for resizing empty arrays
mi = map(SignedMultiplicativeInverse, strds1)
ReshapedArray(parent, dims, reverse(mi))
end

function _reshape(p::Tuple{AbstractArray,LinearFast}, dims::Dims)
parent = p[1]
ReshapedArray(parent, dims, ())
end

@inline size_strides(A::AbstractArray) = tail(size_strides((1,), size(A)...))
size_strides(out::Tuple) = out
@inline size_strides(out, s, sz...) = size_strides((out..., out[end]*s), sz...)

size(A::ReshapedArray) = A.dims
size(A::ReshapedArray, d) = d <= ndims(A) ? A.dims[d] : 1
similar(A::ReshapedArray, eltype::Type) = similar(parent(A), eltype, size(A))
similar(A::ReshapedArray, eltype::Type, dims...) = similar(parent(A), eltype, dims...)
linearindexing{R<:ReshapedArrayLF}(::Type{R}) = LinearFast()
parent(A::ReshapedArray) = A.parent
parentindexes(A::ReshapedArray) = map(s->1:s, size(parent(A)))
reinterpret{T}(::Type{T}, A::ReshapedArray, dims::Dims) = reinterpret(T, parent(A), dims)

@inline ind2sub_rs(::Tuple{}, i::Int) = i
@inline ind2sub_rs(strds, i) = ind2sub_rs((), strds, i-1)
@inline ind2sub_rs(out, ::Tuple{}, ind) = (ind+1, out...)
@inline function ind2sub_rs(out, strds, ind)
d, r = divrem(ind, strds[1])
ind2sub_rs((d+1, out...), tail(strds), r)
end

@inline getindex(A::ReshapedArrayLF, index::Int) = (@boundscheck checkbounds(A, index); @inbounds ret = parent(A)[index]; ret)
@inline getindex(A::ReshapedArray, indexes::Int...) = (@boundscheck checkbounds(A, indexes...); _unsafe_getindex(A, indexes...))
@inline getindex(A::ReshapedArray, index::ReshapedIndex) = (@boundscheck checkbounds(parent(A), index.parentindex); @inbounds ret = parent(A)[index.parentindex]; ret)

@inline _unsafe_getindex(A::ReshapedArray, indexes::Int...) = (@inbounds ret = parent(A)[ind2sub_rs(A.mi, sub2ind(size(A), indexes...))...]; ret)
@inline _unsafe_getindex(A::ReshapedArrayLF, indexes::Int...) = (@inbounds ret = parent(A)[sub2ind(size(A), indexes...)]; ret)

@inline setindex!(A::ReshapedArrayLF, val, index::Int) = (@boundscheck checkbounds(A, index); @inbounds parent(A)[index] = val; val)
@inline setindex!(A::ReshapedArray, val, indexes::Int...) = (@boundscheck checkbounds(A, indexes...); _unsafe_setindex!(A, val, indexes...))
@inline setindex!(A::ReshapedArray, val, index::ReshapedIndex) = (@boundscheck checkbounds(parent(A), index.parentindex); @inbounds parent(A)[index.parentindex] = val; val)

@inline _unsafe_setindex!(A::ReshapedArray, val, indexes::Int...) = (@inbounds parent(A)[ind2sub_rs(A.mi, sub2ind(size(A), indexes...))...] = val; val)
@inline _unsafe_setindex!(A::ReshapedArrayLF, val, indexes::Int...) = (@inbounds parent(A)[sub2ind(size(A), indexes...)] = val; val)

typealias ArrayT{N, T} Array{T,N}
convert{T,S,N}(::Type{Array{T,N}}, V::ReshapedArray{S,N}) = copy!(Array(T, size(V)), V)
convert{T,N}(::Type{ArrayT{N}}, V::ReshapedArray{T,N}) = copy!(Array(T, size(V)), V)

unsafe_convert{T}(::Type{Ptr{T}}, a::ReshapedArray{T}) = unsafe_convert(Ptr{T}, parent(a))
unsafe_convert{T,N,P<:ReshapedArray,I<:Tuple{Vararg{Union{RangeIndex, NoSlice}}}}(::Type{Ptr{T}}, V::SubArray{T,N,P,I}) =
unsafe_convert(Ptr{T}, V.parent) + (first_index(V)-1)*sizeof(T)
6 changes: 5 additions & 1 deletion base/sharedarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,11 @@ length(S::SharedArray) = prod(S.dims)
size(S::SharedArray) = S.dims
linearindexing{S<:SharedArray}(::Type{S}) = LinearFast()

function reshape{T,N}(a::SharedArray{T}, dims::NTuple{N,Int})
reshape(a::SharedVector, dims::Tuple{Int}) = reshape_sa(a, dims)
reshape(a::SharedArray, dims::Tuple{Int}) = reshape_sa(a, dims)
reshape{N}(a::SharedArray, dims::NTuple{N,Int}) = reshape_sa(a, dims)

function reshape_sa{T,N}(a::SharedArray{T}, dims::NTuple{N,Int})
(length(a) != prod(dims)) && throw(DimensionMismatch("dimensions must be consistent with array size"))
refs = Array(Future, length(a.pids))
for (i, p) in enumerate(a.pids)
Expand Down
1 change: 1 addition & 0 deletions base/sparse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module SparseArrays

using Base: Func, AddFun, OrFun, ConjFun, IdFun
using Base: ReshapedArray
using Base.Sort: Forward
using Base.LinAlg: AbstractTriangular, PosDefException

Expand Down
Loading

0 comments on commit 97f80c3

Please sign in to comment.