From 9117b4d6d6f90d53216dcb2c0fc8a0bf8627ccd6 Mon Sep 17 00:00:00 2001 From: Nicholas Bauer Date: Thu, 20 May 2021 12:42:25 -0400 Subject: [PATCH] Syntax for multidimensional arrays (#33697) Co-authored-by: Matt Bauman Co-authored-by: Jeff Bezanson Co-authored-by: Simeon Schaub --- NEWS.md | 7 +- base/abstractarray.jl | 369 ++++++++++++++++++++++++++++++++++++++- base/arrayshow.jl | 52 ++++-- base/exports.jl | 1 + base/reducedim.jl | 4 +- doc/src/base/arrays.md | 1 + doc/src/devdocs/ast.md | 36 ++-- doc/src/manual/arrays.md | 144 +++++++++++++-- src/ast.scm | 13 ++ src/julia-parser.scm | 173 ++++++++++++------ src/julia-syntax.scm | 121 ++++++++++++- test/abstractarray.jl | 45 ++++- test/offsetarray.jl | 6 +- test/show.jl | 11 ++ test/syntax.jl | 20 +++ 15 files changed, 894 insertions(+), 109 deletions(-) diff --git a/NEWS.md b/NEWS.md index 93e3b6e523992..169ebd2fa742e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,11 @@ New language features as `.&&` and `.||`. ([#39594]) * `⫪` (U+2AEA, `\Top`, `\downvDash`) and `⫫` (U+2AEB, `\Bot`, `\upvDash`, `\indep`) may now be used as binary operators with comparison precedence. ([#39403]) +* Repeated semicolons may now be used inside array literals to separate dimensions of an array, + with the number of semicolons specifying the particular dimension. Just as the single semicolon + in `[A; B]` has always described concatenating along the first dimension (vertically), now two + semicolons `[A;; B]` do so in the second dimension (horizontally), three semicolons `;;;` in the + third, and so on. ([#33697]) Language changes ---------------- @@ -148,7 +153,7 @@ Standard library changes Deprecated or removed --------------------- -- Multiple successive semicolons in an array expresion were previously ignored (e.g. `[1 ;; 2] == [1 ; 2]`). Multiple semicolons are being reserved for future syntax and may have different behavior in a future release. +- Multiple successive semicolons in an array expresion were previously ignored (e.g., `[1 ;; 2] == [1 ; 2]`). This is now being used to separate dimensions for array literals. (see **New language features**) External dependencies diff --git a/base/abstractarray.jl b/base/abstractarray.jl index 2cbe4b501e718..fd5d631a08c44 100644 --- a/base/abstractarray.jl +++ b/base/abstractarray.jl @@ -1829,6 +1829,25 @@ dimensions for every new input array and putting zero blocks elsewhere. For exam `cat(matrices...; dims=(1,2))` builds a block diagonal matrix, i.e. a block matrix with `matrices[1]`, `matrices[2]`, ... as diagonal blocks and matching zero blocks away from the diagonal. + +julia> cat(a, b, dims=3) +2×2×4 Array{Int64, 3}: +[:, :, 1] = + 1 2 + 3 4 + +[:, :, 2] = + 5 6 + 7 8 + +[:, :, 3] = + 9 10 + 11 12 + +[:, :, 4] = + 13 14 + 15 16 +``` """ @inline cat(A...; dims) = _cat(dims, A...) _cat(catdims, A::AbstractArray{T}...) where {T} = cat_t(T, A...; dims=catdims) @@ -1886,7 +1905,7 @@ julia> hvcat((3,3), a,b,c,d,e,f) 1 2 3 4 5 6 -julia> [a b;c d; e f] +julia> [a b; c d; e f] 3×2 Matrix{Int64}: 1 2 3 4 @@ -2013,6 +2032,354 @@ function typed_hvcat(::Type{T}, rows::Tuple{Vararg{Int}}, as...) where T T[rs...;] end +# nd concatenation + +""" + hvncat(dims::Tuple{Vararg{Int}}, row_first, values...) + hvncat(shape::Tuple{Vararg{Tuple}}, row_first, values...) + +Horizontal, vertical, and n-dimensional concatenation of many `values` in one call. + +This function is called +for block matrix syntax. The first argument either specifies the shape of the concatenation, +similar to `hvcat`, as a tuple of tuples, or the dimensions that specify the key number of +elements along each axis, and is used to determine the output dimensions. The `dims` form +is more performant, and is used by default when the concatenation operation has the same +number of elements along each axis (e.g., [a b; c d;;; e f ; g h]). The `shape` form is used +when the number of elements along each axis is unbalanced (e.g., [a b ; c]). Unbalanced +syntax needs additional validation overhead. + +# Examples +```jldoctest +julia> a, b, c, d, e, f = 1, 2, 3, 4, 5, 6 +(1, 2, 3, 4, 5, 6) + +julia> [a b c;;; d e f] +1×3×2 Array{Int64, 3}: +[:, :, 1] = + 1 2 3 + +[:, :, 2] = + 4 5 6 + +julia> hvncat((2,1,3), false, a,b,c,d,e,f) +2×1×3 Array{Int64, 3}: +[:, :, 1] = + 1 + 2 + +[:, :, 2] = + 3 + 4 + +[:, :, 3] = + 5 + 6 + +julia> [a b;;; c d;;; e f] +1×2×3 Array{Int64, 3}: +[:, :, 1] = + 1 2 + +[:, :, 2] = + 3 4 + +[:, :, 3] = + 5 6 + +julia> hvncat(((3, 3), (3, 3), (6,)), true, a, b, c, d, e, f) +1×3×2 Array{Int64, 3}: +[:, :, 1] = + 1 2 3 + +[:, :, 2] = + 4 5 6 +``` +""" +hvncat(::Tuple{}, ::Bool) = [] +hvncat(::Tuple{}, ::Bool, xs...) = [] +hvncat(::Tuple{Vararg{Any, 1}}, ::Bool, xs...) = vcat(xs...) # methods assume 2+ dimensions +hvncat(dimsshape::Tuple, row_first::Bool, xs...) = _hvncat(dimsshape, row_first, xs...) +hvncat(dim::Int, xs...) = _hvncat(dim, true, xs...) + +_hvncat(::Union{Tuple, Int}, ::Bool) = [] +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs...) = _typed_hvncat(promote_eltypeof(xs...), dimsshape, row_first, xs...) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::T...) where T<:Number = _typed_hvncat(T, dimsshape, row_first, xs...) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::Number...) = _typed_hvncat(promote_typeof(xs...), dimsshape, row_first, xs...) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::AbstractArray...) = _typed_hvncat(promote_eltype(xs...), dimsshape, row_first, xs...) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::AbstractArray{T}...) where T = _typed_hvncat(T, dimsshape, row_first, xs...) + +typed_hvncat(::Type{T}, ::Tuple{}, ::Bool) where T = Vector{T}() +typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, xs...) where T = Vector{T}() +typed_hvncat(T::Type, ::Tuple{Vararg{Any, 1}}, ::Bool, xs...) = typed_vcat(T, xs...) # methods assume 2+ dimensions +typed_hvncat(T::Type, dimsshape::Tuple, row_first::Bool, xs...) = _typed_hvncat(T, dimsshape, row_first, xs...) +typed_hvncat(T::Type, dim::Int, xs...) = _typed_hvncat(T, Val(dim), xs...) + +_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool) where T = Vector{T}() +_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, xs...) where T = Vector{T}() +_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, xs::Number...) where T = Vector{T}() +function _typed_hvncat(::Type{T}, dims::Tuple{Vararg{Int, N}}, row_first::Bool, xs::Number...) where {T, N} + A = Array{T, N}(undef, dims...) + lengtha = length(A) # Necessary to store result because throw blocks are being deoptimized right now, which leads to excessive allocations + lengthx = length(xs) # Cuts from 3 allocations to 1. + if lengtha != lengthx + throw(ArgumentError("argument count does not match specified shape (expected $lengtha, got $lengthx)")) + end + hvncat_fill!(A, row_first, xs) + return A +end + +function hvncat_fill!(A::Array, row_first::Bool, xs::Tuple) + # putting these in separate functions leads to unnecessary allocations + if row_first + nr, nc = size(A, 1), size(A, 2) + nrc = nr * nc + @inbounds na = prod(size(A)[3:end]) + k = 1 + @inbounds for d ∈ 1:na + dd = nrc * (d - 1) + for i ∈ 1:nr + Ai = dd + i + for j ∈ 1:nc + A[Ai] = xs[k] + k += 1 + Ai += nr + end + end + end + else + @inbounds for k ∈ eachindex(xs) + A[k] = xs[k] + end + end +end + +_typed_hvncat(T::Type, dim::Int, ::Bool, xs...) = _typed_hvncat(T, Val(dim), xs...) # catches from _hvncat type promoters +_typed_hvncat(::Type{T}, ::Val) where T = Vector{T}() +_typed_hvncat(T::Type, ::Val{N}, xs::Number...) where N = _typed_hvncat(T, (ntuple(x -> 1, N - 1)..., length(xs)), false, xs...) +function _typed_hvncat(::Type{T}, ::Val{N}, as::AbstractArray...) where {T, N} + # optimization for arrays that can be concatenated by copying them linearly into the destination + # conditions: the elements must all have 1- or 0-length dimensions above N + @inbounds for a ∈ as + ndims(a) <= N || all(x -> size(a, x) == 1, (N + 1):ndims(a)) || + return _typed_hvncat(T, (ntuple(x -> 1, N - 1)..., length(as)), false, as...) + end + + nd = max(N, ndims(as[1])) + + Ndim = 0 + @inbounds for i ∈ 1:lastindex(as) + Ndim += cat_size(as[i], N) + for d ∈ 1:N - 1 + cat_size(as[1], d) == cat_size(as[i], d) || throw(ArgumentError("mismatched size along axis $d in element $i")) + end + end + + @inbounds A = Array{T, nd}(undef, ntuple(d -> cat_size(as[1], d), N - 1)..., Ndim, ntuple(x -> 1, nd - N)...) + k = 1 + @inbounds for a ∈ as + for i ∈ eachindex(a) + A[k] = a[i] + k += 1 + end + end + return A +end + +function _typed_hvncat(::Type{T}, ::Val{N}, as...) where {T, N} + # optimization for scalars and 1-length arrays that can be concatenated by copying them linearly + # into the destination + nd = N + Ndim = 0 + @inbounds for a ∈ as + if a isa AbstractArray + cat_size(a, N) == length(a) || + throw(ArgumentError("all dimensions of elements other than $N must be of length 1")) + nd = max(nd, ndims(a)) + end + Ndim += cat_size(a, N) + end + + @inbounds A = Array{T, nd}(undef, ntuple(x -> 1, N - 1)..., Ndim, ntuple(x -> 1, nd - N)...) + + k = 1 + @inbounds for a ∈ as + if a isa AbstractArray + lena = length(a) + copyto!(A, k, a, 1, lena) + k += lena + else + A[k] = a + k += 1 + end + end + return A +end + +function _typed_hvncat(::Type{T}, dims::Tuple{Vararg{Int, N}}, row_first::Bool, as...) where {T, N} + d1 = row_first ? 2 : 1 + d2 = row_first ? 1 : 2 + + # discover dimensions + @inbounds nd = max(N, ndims(as[1])) + outdims = zeros(Int, nd) + + # discover number of rows or columns + @inbounds for i ∈ 1:dims[d1] + outdims[d1] += cat_size(as[i], d1) + end + + currentdims = zeros(Int, nd) + blockcount = 0 + @inbounds for i ∈ eachindex(as) + currentdims[d1] += cat_size(as[i], d1) + if currentdims[d1] == outdims[d1] + currentdims[d1] = 0 + for d ∈ (d2, 3:nd...) + currentdims[d] += cat_size(as[i], d) + if outdims[d] == 0 # unfixed dimension + blockcount += 1 + if blockcount == (d > length(dims) ? 1 : dims[d]) # last expected member of dimension + outdims[d] = currentdims[d] + currentdims[d] = 0 + blockcount = 0 + else + break + end + else # fixed dimension + if currentdims[d] == outdims[d] # end of dimension + currentdims[d] = 0 + elseif currentdims[d] < outdims[d] # dimension in progress + break + else # exceeded dimension + ArgumentError("argument $i has too many elements along axis $d") |> throw + end + end + end + elseif currentdims[d1] > outdims[d1] # exceeded dimension + ArgumentError("argument $i has too many elements along axis $d1") |> throw + end + end + + # calling sum() leads to 3 extra allocations + len = 0 + for a ∈ as + len += cat_length(a) + end + outlen = prod(outdims) + outlen == 0 && ArgumentError("too few elements in arguments, unable to infer dimensions") |> throw + len == outlen || ArgumentError("too many elements in arguments; expected $(outlen), got $(len)") |> throw + + # copy into final array + A = Array{T, nd}(undef, outdims...) + # @assert all(==(0), currentdims) + outdims .= 0 + hvncat_fill!(A, currentdims, outdims, d1, d2, as) + return A +end + +function _typed_hvncat(::Type{T}, shape::Tuple{Vararg{Tuple, N}}, row_first::Bool, as...) where {T, N} + d1 = row_first ? 2 : 1 + d2 = row_first ? 1 : 2 + shape = collect(shape) # saves allocations later + @inbounds shapelength = shape[end][1] + lengthas = length(as) + shapelength == lengthas || throw(ArgumentError("number of elements does not match shape; expected $(shapelength), got $lengthas)")) + + # discover dimensions + @inbounds nd = max(N, ndims(as[1])) + outdims = zeros(Int, nd) + currentdims = zeros(Int, nd) + blockcounts = zeros(Int, nd) + shapepos = ones(Int, nd) + + @inbounds for i ∈ eachindex(as) + wasstartblock = false + for d ∈ 1:N + ad = (d < 3 && row_first) ? (d == 1 ? 2 : 1) : d + dsize = cat_size(as[i], ad) + blockcounts[d] += 1 + + if d == 1 || i == 1 || wasstartblock + currentdims[d] += dsize + elseif dsize != cat_size(as[i - 1], ad) + ArgumentError("argument $i has a mismatched number of elements along axis $ad; expected $(cat_size(as[i - 1], ad)), got $dsize") |> throw + end + + wasstartblock = blockcounts[d] == 1 # remember for next dimension + + isendblock = blockcounts[d] == shape[d][shapepos[d]] + if isendblock + if outdims[d] == 0 + outdims[d] = currentdims[d] + elseif outdims[d] != currentdims[d] + ArgumentError("argument $i has a mismatched number of elements along axis $ad; expected $(abs(outdims[d] - (currentdims[d] - dsize))), got $dsize") |> throw + end + currentdims[d] = 0 + blockcounts[d] = 0 + shapepos[d] += 1 + end + end + end + + if row_first + @inbounds outdims[1], outdims[2] = outdims[2], outdims[1] + end + + # @assert all(==(0), currentdims) + # @assert all(==(0), blockcounts) + + # copy into final array + A = Array{T, nd}(undef, outdims...) + hvncat_fill!(A, currentdims, blockcounts, d1, d2, as) + return A +end + +@inline function hvncat_fill!(A::Array{T, N}, scratch1::Vector{Int}, scratch2::Vector{Int}, d1::Int, d2::Int, as::Tuple{Vararg}) where {T, N} + outdims = size(A) + offsets = scratch1 + inneroffsets = scratch2 + @inbounds for a ∈ as + if isa(a, AbstractArray) + for ai ∈ a + Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) + A[Ai] = ai + + for j ∈ 1:N + inneroffsets[j] += 1 + inneroffsets[j] < cat_size(a, j) && break + inneroffsets[j] = 0 + end + end + else + Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) + A[Ai] = a + end + + for j ∈ (d1, d2, 3:N...) + offsets[j] += cat_size(a, j) + offsets[j] < outdims[j] && break + offsets[j] = 0 + end + end +end + +@propagate_inbounds function hvncat_calcindex(offsets::Vector{Int}, inneroffsets::Vector{Int}, + outdims::Tuple{Vararg{Int}}, nd::Int) + Ai = inneroffsets[1] + offsets[1] + 1 + for j ∈ 2:nd + increment = inneroffsets[j] + offsets[j] + for k ∈ 1:j-1 + increment *= outdims[k] + end + Ai += increment + end + Ai +end + +cat_length(a::AbstractArray) = length(a) +cat_length(::Any) = 1 + ## Reductions and accumulates ## function isequal(A::AbstractArray, B::AbstractArray) diff --git a/base/arrayshow.jl b/base/arrayshow.jl index f942f87787484..1e9f3e59729e6 100644 --- a/base/arrayshow.jl +++ b/base/arrayshow.jl @@ -271,17 +271,21 @@ end # typeinfo agnostic # n-dimensional arrays -show_nd(io::IO, a::AbstractArray, print_matrix::Function, label_slices::Bool) = - _show_nd(io, inferencebarrier(a), print_matrix, label_slices, map(unitrange, axes(a))) +show_nd(io::IO, a::AbstractArray, print_matrix::Function, show_full::Bool) = + _show_nd(io, inferencebarrier(a), print_matrix, show_full, map(unitrange, axes(a))) -function _show_nd(io::IO, @nospecialize(a::AbstractArray), print_matrix::Function, label_slices::Bool, axs::Tuple{Vararg{AbstractUnitRange}}) +function _show_nd(io::IO, @nospecialize(a::AbstractArray), print_matrix::Function, show_full::Bool, axs::Tuple{Vararg{AbstractUnitRange}}) limit::Bool = get(io, :limit, false) if isempty(a) return end tailinds = tail(tail(axs)) nd = ndims(a)-2 - for I in CartesianIndices(tailinds) + show_full || print(io, "[") + Is = CartesianIndices(tailinds) + lastidxs = first(Is).I + reached_last_d = false + for I in Is idxs = I.I if limit for i = 1:nd @@ -296,7 +300,9 @@ function _show_nd(io::IO, @nospecialize(a::AbstractArray), print_matrix::Functio @goto skip end end - print(io, "...\n\n") + print(io, ";"^(i+2)) + print(io, " \u2026 ") + show_full && print(io, "\n\n") @goto skip end if ind[firstindex(ind)+2] < ii <= ind[end-3] @@ -305,13 +311,29 @@ function _show_nd(io::IO, @nospecialize(a::AbstractArray), print_matrix::Functio end end end - if label_slices + if show_full _show_nd_label(io, a, idxs) end slice = view(a, axs[1], axs[2], idxs...) - print_matrix(io, slice) - print(io, idxs == map(last,tailinds) ? "" : "\n\n") + if show_full + print_matrix(io, slice) + print(io, idxs == map(last,tailinds) ? "" : "\n\n") + else + idxdiff = lastidxs .- idxs .< 0 + if any(idxdiff) + lastchangeindex = 2 + findlast(idxdiff) + print(io, ";"^lastchangeindex) + lastchangeindex == ndims(a) && (reached_last_d = true) + print(io, " ") + end + print_matrix(io, slice) + end @label skip + lastidxs = idxs + end + if !show_full + reached_last_d || print(io, ";"^(nd+2)) + print(io, "]") end end @@ -386,9 +408,9 @@ end preceded by `prefix`, supposed to encode the type of the elements. """ _show_nonempty(io::IO, X::AbstractMatrix, prefix::String) = - _show_nonempty(io, inferencebarrier(X), prefix, axes(X)) + _show_nonempty(io, inferencebarrier(X), prefix, false, axes(X)) -function _show_nonempty(io::IO, @nospecialize(X::AbstractMatrix), prefix::String, axs::Tuple{AbstractUnitRange,AbstractUnitRange}) +function _show_nonempty(io::IO, @nospecialize(X::AbstractMatrix), prefix::String, drop_brackets::Bool, axs::Tuple{AbstractUnitRange,AbstractUnitRange}) @assert !isempty(X) limit = get(io, :limit, false)::Bool indr, indc = axs @@ -407,7 +429,7 @@ function _show_nonempty(io::IO, @nospecialize(X::AbstractMatrix), prefix::String cdots = true end end - print(io, prefix, "[") + drop_brackets || print(io, prefix, "[") for rr in (rr1, rr2) for i in rr for cr in (cr1, cr2) @@ -429,12 +451,16 @@ function _show_nonempty(io::IO, @nospecialize(X::AbstractMatrix), prefix::String end last(rr) != last(indr) && rdots && print(io, "\u2026 ; ") end - print(io, "]") + if !drop_brackets + nc > 1 || print(io, ";;") + print(io, "]") + end + return nothing end _show_nonempty(io::IO, X::AbstractArray, prefix::String) = - show_nd(io, X, (io, slice) -> _show_nonempty(io, slice, prefix), false) + show_nd(io, X, (io, slice) -> _show_nonempty(io, inferencebarrier(slice), prefix, true, axes(slice)), false) # a specific call path is used to show vectors (show_vector) _show_nonempty(::IO, ::AbstractVector, ::String) = diff --git a/base/exports.jl b/base/exports.jl index 7a2f25072b8cd..36a78c544acae 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -391,6 +391,7 @@ export first, hcat, hvcat, + hvncat, indexin, argmax, argmin, diff --git a/base/reducedim.jl b/base/reducedim.jl index ff2b24db740ef..5851cfcc0cd1f 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -999,7 +999,7 @@ julia> findmin(A, dims=1) ([1.0 2.0], CartesianIndex{2}[CartesianIndex(1, 1) CartesianIndex(1, 2)]) julia> findmin(A, dims=2) -([1.0; 3.0], CartesianIndex{2}[CartesianIndex(1, 1); CartesianIndex(2, 1)]) +([1.0; 3.0;;], CartesianIndex{2}[CartesianIndex(1, 1); CartesianIndex(2, 1);;]) ``` """ findmin(A::AbstractArray; dims=:) = _findmin(A, dims) @@ -1046,7 +1046,7 @@ julia> findmax(A, dims=1) ([3.0 4.0], CartesianIndex{2}[CartesianIndex(2, 1) CartesianIndex(2, 2)]) julia> findmax(A, dims=2) -([2.0; 4.0], CartesianIndex{2}[CartesianIndex(1, 2); CartesianIndex(2, 2)]) +([2.0; 4.0;;], CartesianIndex{2}[CartesianIndex(1, 2); CartesianIndex(2, 2);;]) ``` """ findmax(A::AbstractArray; dims=:) = _findmax(A, dims) diff --git a/doc/src/base/arrays.md b/doc/src/base/arrays.md index 0493ae1e35e72..1dc2d8ed926af 100644 --- a/doc/src/base/arrays.md +++ b/doc/src/base/arrays.md @@ -141,6 +141,7 @@ Base.cat Base.vcat Base.hcat Base.hvcat +Base.hvncat Base.vect Base.circshift Base.circshift! diff --git a/doc/src/devdocs/ast.md b/doc/src/devdocs/ast.md index af89290618fec..d02606840c431 100644 --- a/doc/src/devdocs/ast.md +++ b/doc/src/devdocs/ast.md @@ -63,23 +63,25 @@ call. Finally, chains of comparisons have their own special expression structure ### Bracketed forms -| Input | AST | -|:------------------------ |:------------------------------------ | -| `a[i]` | `(ref a i)` | -| `t[i;j]` | `(typed_vcat t i j)` | -| `t[i j]` | `(typed_hcat t i j)` | -| `t[a b; c d]` | `(typed_vcat t (row a b) (row c d))` | -| `a{b}` | `(curly a b)` | -| `a{b;c}` | `(curly a (parameters c) b)` | -| `[x]` | `(vect x)` | -| `[x,y]` | `(vect x y)` | -| `[x;y]` | `(vcat x y)` | -| `[x y]` | `(hcat x y)` | -| `[x y; z t]` | `(vcat (row x y) (row z t))` | -| `[x for y in z, a in b]` | `(comprehension x (= y z) (= a b))` | -| `T[x for y in z]` | `(typed_comprehension T x (= y z))` | -| `(a, b, c)` | `(tuple a b c)` | -| `(a; b; c)` | `(block a (block b c))` | +| Input | AST | +|:------------------------ |:------------------------------------------------- | +| `a[i]` | `(ref a i)` | +| `t[i;j]` | `(typed_vcat t i j)` | +| `t[i j]` | `(typed_hcat t i j)` | +| `t[a b; c d]` | `(typed_vcat t (row a b) (row c d))` | +| `t[a b;;; c d]` | `(typed_ncat t 3 (row a b) (row c d))` | +| `a{b}` | `(curly a b)` | +| `a{b;c}` | `(curly a (parameters c) b)` | +| `[x]` | `(vect x)` | +| `[x,y]` | `(vect x y)` | +| `[x;y]` | `(vcat x y)` | +| `[x y]` | `(hcat x y)` | +| `[x y; z t]` | `(vcat (row x y) (row z t))` | +| `[x;y;; z;t;;;]` | `(ncat 3 (nrow 2 (nrow 1 x y) (nrow 1 z t)))` | +| `[x for y in z, a in b]` | `(comprehension x (= y z) (= a b))` | +| `T[x for y in z]` | `(typed_comprehension T x (= y z))` | +| `(a, b, c)` | `(tuple a b c)` | +| `(a; b; c)` | `(block a (block b c))` | ### Macros diff --git a/doc/src/manual/arrays.md b/doc/src/manual/arrays.md index 2afc264556713..c3c014ccb64fe 100644 --- a/doc/src/manual/arrays.md +++ b/doc/src/manual/arrays.md @@ -5,8 +5,8 @@ technical computing languages pay a lot of attention to their array implementati of other containers. Julia does not treat arrays in any special way. The array library is implemented almost completely in Julia itself, and derives its performance from the compiler, just like any other code written in Julia. As such, it's also possible to define custom array types by inheriting -from [`AbstractArray`](@ref). See the [manual section on the AbstractArray interface](@ref man-interface-array) for more details -on implementing a custom array type. +from [`AbstractArray`](@ref). See the [manual section on the AbstractArray interface](@ref man-interface-array) +for more details on implementing a custom array type. An array is a collection of objects stored in a multi-dimensional grid. In the most general case, an array may contain objects of type [`Any`](@ref). For most computational purposes, arrays should contain @@ -126,7 +126,7 @@ Any[] ### [Concatenation](@id man-array-concatenation) -If the arguments inside the square brackets are separated by semicolons (`;`) or newlines +If the arguments inside the square brackets are separated by single semicolons (`;`) or newlines instead of commas, then their contents are _vertically concatenated_ together instead of the arguments being used as elements themselves. @@ -154,7 +154,7 @@ julia> [1:2 6 ``` -Similarly, if the arguments are separated by tabs or spaces, then their contents are +Similarly, if the arguments are separated by tabs or spaces or double semicolons, then their contents are _horizontally concatenated_ together. ```jldoctest @@ -171,9 +171,13 @@ julia> [[1,2] [4,5] [7,8]] julia> [1 2 3] # Numbers can also be horizontally concatenated 1×3 Matrix{Int64}: 1 2 3 + +julia> [1;; 2;; 3;; 4] +1×4 Matrix{Int64}: + 1 2 3 4 ``` -Using semicolons (or newlines) and spaces (or tabs) can be combined to concatenate +Single semicolons (or newlines) and spaces (or tabs) can be combined to concatenate both horizontally and vertically at the same time. ```jldoctest @@ -189,17 +193,135 @@ julia> [zeros(Int, 2, 2) [1; 2] 0 0 1 0 0 2 3 4 5 + +julia> [[1 1]; 2 3; [4 4]] +3×2 Matrix{Int64}: + 1 1 + 2 3 + 4 4 +``` + +Spaces (and tabs) have a higher precedence than semicolons, performing any horizontal +concatenations first and then concatenating the result. Using double semicolons for the +horizontal concatenation, on the other hand, performs any vertical concatenations before +horizontally concatenating the result. + +```jldoctest +julia> [zeros(Int, 2, 2) ; [3 4] ;; [1; 2] ; 5] +3×3 Matrix{Int64}: + 0 0 1 + 0 0 2 + 3 4 5 + +julia> [1:2; 4;; 1; 3:4] +3×2 Matrix{Int64}: + 1 1 + 2 3 + 4 4 +``` + +Just as `;` and `;;` concatenate in the first and second dimension, using more semicolons +extends this same general scheme. The number of semicolons in the separator specifies the +particular dimension, so `;;;` concetenates in the third dimension, `;;;;` in the 4th, and +so on. Fewer semicolons take precedence, so the lower dimensions are generally concatenated +first. + +```jldoctest +julia> [1; 2;; 3; 4;; 5; 6;;; + 7; 8;; 9; 10;; 11; 12] +2×3×2 Array{Int64, 3}: +[:, :, 1] = + 1 3 5 + 2 4 6 + +[:, :, 2] = + 7 9 11 + 8 10 12 +``` + +Like before, spaces (and tabs) for horizontal concatenation have a higher precedence than +any number of semicolons. Thus, higher dimensional arrays can also be written by specifying +their rows first, with their elements textually arranged in a manner similar to their layout: + +```jldoctest +julia> [1 3 5 + 2 4 6;;; + 7 9 11 + 8 10 12] +2×3×2 Array{Int64, 3}: +[:, :, 1] = + 1 3 5 + 2 4 6 + +[:, :, 2] = + 7 9 11 + 8 10 12 + +julia> [1 2;;; 3 4;;;; 5 6;;; 7 8] +1×2×2×2 Array{Int64, 4}: +[:, :, 1, 1] = + 1 2 + +[:, :, 2, 1] = + 3 4 + +[:, :, 1, 2] = + 5 6 + +[:, :, 2, 2] = + 7 8 + +julia> [[1 2;;; 3 4];;;; [5 6];;; [7 8]] +1×2×2×2 Array{Int64, 4}: +[:, :, 1, 1] = + 1 2 + +[:, :, 2, 1] = + 3 4 + +[:, :, 1, 2] = + 5 6 + +[:, :, 2, 2] = + 7 8 +``` + +Although they both mean concatenation in the second dimension, spaces (or tabs) and `;;` +cannot appear in the same array expression unless the double semicolon is simply serving as +a "line continuation" character. This allows a single horizontal concatenation to span +multiple lines (without the line break being interpreted as a vertical concatenation). + +```jldoctest +julia> [1 2 ;; + 3 4] +1×4 Matrix{Int64}: + 1 2 3 4 +``` + +Terminating semicolons may also be used to add trailing length 1 dimensions. + +```jldoctest +julia> [1;;] +1×1 Matrix{Int64}: + 1 + +julia> [2; 3;;;] +2×1×1 Array{Int64, 3}: +[:, :, 1] = + 2 + 3 ``` More generally, concatenation can be accomplished through the [`cat`](@ref) function. These syntaxes are shorthands for function calls that themselves are convenience functions: -| Syntax | Function | Description | -|:----------------- |:--------------- |:-------------------------------------------------- | -| | [`cat`](@ref) | concatenate input arrays along dimension(s) `k` | -| `[A; B; C; ...]` | [`vcat`](@ref) | shorthand for `cat(A...; dims=1) | -| `[A B C ...]` | [`hcat`](@ref) | shorthand for `cat(A...; dims=2) | -| `[A B; C D; ...]` | [`hvcat`](@ref) | simultaneous vertical and horizontal concatenation | +| Syntax | Function | Description | +|:---------------------- |:---------------- |:---------------------------------------------------------------------------------------------------------- | +| | [`cat`](@ref) | concatenate input arrays along dimension(s) `k` | +| `[A; B; C; ...]` | [`vcat`](@ref) | shorthand for `cat(A...; dims=1) | +| `[A B C ...]` | [`hcat`](@ref) | shorthand for `cat(A...; dims=2) | +| `[A B; C D; ...]` | [`hvcat`](@ref) | simultaneous vertical and horizontal concatenation | +| `[A; C;; B; D;;; ...]` | [`hvncat`](@ref) | simultaneous n-dimensional concatenation, where number of semicolons indicate the dimension to concatenate | ### Typed array literals diff --git a/src/ast.scm b/src/ast.scm index 6ed530718e3db..d89cae95ad185 100644 --- a/src/ast.scm +++ b/src/ast.scm @@ -61,6 +61,12 @@ (else (string e)))) +(define (deparse-semicolons n) + ; concatenate n semicolons + (if (<= n 0) + "" + (string ";" (deparse-semicolons (1- n))))) + (define (deparse e (ilvl 0)) (cond ((or (symbol? e) (number? e)) (string e)) ((string? e) (print-to-string e)) @@ -134,7 +140,14 @@ ((hcat) (string #\[ (deparse-arglist (cdr e) " ") #\])) ((typed_hcat) (string (deparse (cadr e)) (deparse (cons 'hcat (cddr e))))) + ((ncat) (string #\[ (deparse-arglist (cddr e) (string (deparse-semicolons (cadr e)) " ")) + (if (= (length (cddr e)) 1) + (deparse-semicolons (cadr e)) + "") #\])) + ((typed_ncat) (string (deparse (cadr e)) + (deparse (cons 'ncat (cddr e))))) ((row) (deparse-arglist (cdr e) " ")) + ((nrow) (deparse-arglist (cddr e) (string (deparse-semicolons (cadr e)) " "))) ((braces) (string #\{ (deparse-arglist (cdr e) ", ") #\})) ((bracescat) (string #\{ (deparse-arglist (cdr e) "; ") #\})) ((string) diff --git a/src/julia-parser.scm b/src/julia-parser.scm index c6510dcbd9536..9f176f95829db 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -1218,6 +1218,8 @@ (loop (list* 'typed_vcat ex (cdr al)))) ((comprehension) (loop (list* 'typed_comprehension ex (cdr al)))) + ((ncat) + (loop (list* 'typed_ncat ex (cdr al)))) (else (error "unknown parse-cat result (internal error)"))))))) ((|.|) (disallow-space s ex t) @@ -1849,60 +1851,123 @@ (take-token s)) `(comprehension ,gen)))) -(define (parse-matrix s first closer gotnewline last-end-symbol) - (define (fix head v) (cons head (reverse v))) - (define (update-outer v outer) - (cond ((null? v) outer) - ((null? (cdr v)) (cons (car v) outer)) - (else (cons (fix 'row v) outer)))) - (define semicolon (eqv? (peek-token s) #\;)) +(define (parse-array s first closer gotnewline last-end-symbol) + (define (fix head v) + (cons head (reverse v))) + (define (unfixrow l) + (cons (reverse (cdaar l)) (if (and (null? (cdar l)) (null? (cdr l))) + '() + (cons (cdar l) (cdr l))))) + (define (fixcat head d v) + (cons head (cons d (reverse v)))) + (define (ncons a n l) + (if (< n 1) + l + (ncons a (1- n) (cons a l)))) + (define (fix-level ah n) + (if (length= ah 1) + (car ah) + (if (= n 1) + (fix 'row ah) + (fixcat 'nrow (1- n) ah)))) + (define (collapse-level n l i) + (if (> n 0) + (let* ((lhfix (fix-level (car l) i)) + (lnew (if (null? (cdr l)) + (list (list lhfix)) + (cons (cons lhfix (cadr l)) (cddr l))))) + (collapse-level (1- n) lnew (1+ i))) + l)) + (define (parse-array-inner s a is-row-first semicolon-count max-level closer gotnewline gotlinesep) + (define (process-semicolon next) + (set! semicolon-count (1+ semicolon-count)) + (set! max-level (max max-level semicolon-count)) + (if (and (null? is-row-first) (= semicolon-count 2) (not (eqv? next #\;))) + ; finding ;; that isn't a row-separator makes it column-first + (set! is-row-first #f)) + (set! a (collapse-level 1 a semicolon-count))) + (define (restore-lower-dim-lists next) + (if (and (not gotlinesep) (not (memv next (list #\; 'for closer #\newline)))) + (set! a (ncons '() semicolon-count a)))) + (let ((t (if (or gotnewline (eqv? (peek-token s) #\newline)) + #\newline + (require-token s)))) + (if (eqv? t closer) + (begin + (take-token s) + (set! a (collapse-level (- max-level semicolon-count) a (1+ semicolon-count))) + (cond ((= max-level 0) + (if (length= (car a) 1) + (fix 'vect (car a)) + (fix 'hcat (car a)))) + ((= max-level 1) + (fix 'vcat (car a))) + (else + (fixcat 'ncat max-level (car a))))) + (case t + ((#\newline) + (or gotnewline (take-token s)) + (let ((next (peek-token s))) + (if (and (> semicolon-count 0) (eqv? next #\;)) + (error (string "semicolons may appear before or after a line break in an array expression, " + "but not both"))) + (if (and (= semicolon-count 0) + (not (memv next (list #\; 'for closer #\newline)))) + ; treat a linebreak prior to a value as a semicolon if no previous semicolons observed + (process-semicolon next)) + (restore-lower-dim-lists next) + (parse-array-inner s a is-row-first semicolon-count max-level closer #f gotlinesep))) + ((#\;) + (or gotnewline (take-token s)) + (let ((next (peek-token s))) + (let ((is-line-sep + (if (and (not (null? is-row-first)) is-row-first (= semicolon-count 1)) + (cond ((eqv? next #\newline) #t) ; [a b ;;... + ((not (or (eof-object? next) (eqv? next #\;))) ; [a b ;;... + (error (string "cannot mix space and ;; separators in an array expression, " + "except to wrap a line"))) + (else #f)) ; [a b ;; for REPL, [a ;;... + #f))) ; [a ; b ;; c ; d... + (if is-line-sep + (begin (set! a (unfixrow a)) + (set! max-level + (if (null? (cdr a)) + 0 ; no prior single semicolon + max-level))) + (begin (process-semicolon next) + (restore-lower-dim-lists next))) + (parse-array-inner s a is-row-first semicolon-count max-level closer #f is-line-sep)))) + ((#\,) + (error "unexpected comma in array expression")) + ((#\] #\}) + (error (string "unexpected \"" t "\""))) + ((for) + (if (and (length= (car a) 1) + (null? (cdr a))) + (begin ;; if we get here, there must have been some kind of space or separator + ;;(expect-space-before s 'for) + (take-token s) + (parse-comprehension s (caar a) closer)) + (error "invalid comprehension syntax"))) + (else + (if (and (not gotlinesep) (pair? (car a)) (not (ts:space? s))) + (error (string "expected \"" closer "\" or separator in arguments to \"" + (if (eqv? closer #\]) #\[ #\{) " " closer + "\"; got \"" + (deparse (caar a)) t "\""))) + (let ((u (parse-eq* s))) + (set! a (cons (cons u (car a)) (cdr a))) + (if (= (length (car a)) 2) + ; at least 2 elements separated by space found [a b...], [a; b c...] + (if (null? is-row-first) + (set! is-row-first #t) + (if (not is-row-first) + (error (string "cannot mix space and \";;\" separators in an array expression, " + "except to wrap a line")))))) + (parse-array-inner s a is-row-first 0 max-level closer #f #f)))))) ;; if a [ ] expression is a cat expression, `end` is not special (with-bindings ((end-symbol last-end-symbol)) - (let loop ((vec (list first)) - (outer '())) - (let ((t (if (or (eqv? (peek-token s) #\newline) gotnewline) - #\newline - (require-token s)))) - (if (eqv? t closer) - (begin (take-token s) - (if (pair? outer) - (fix 'vcat (update-outer vec outer)) - (if (or (null? vec) (null? (cdr vec))) - (fix 'vect vec) ; [x] => (vect x) - (fix 'hcat vec)))) ; [x y] => (hcat x y) - (case t - ((#\;) - (take-token s) - (if (eqv? (peek-token s) #\;) - (parser-depwarn s (string "Multiple semicolons in an array concatenation expression currently have no effect, " - "but may have a new meaning in a future version of Julia.") - "Please remove extra semicolons to preserve forward compatibility e.g. [1;;3] => [1;3].")) - (set! gotnewline #f) - (loop '() (update-outer vec outer))) - ((#\newline) - (or gotnewline (take-token s)) - (set! gotnewline #f) - (loop '() (update-outer vec outer))) - ((#\,) - (error "unexpected comma in matrix expression")) - ((#\] #\}) - (error (string "unexpected \"" t "\""))) - ((for) - (if (and (not semicolon) - (length= outer 1) - (null? vec)) - (begin ;; if we get here, there must have been some kind of space or separator - ;;(expect-space-before s 'for) - (take-token s) - (parse-comprehension s (car outer) closer)) - (error "invalid comprehension syntax"))) - (else - (if (and (pair? vec) (not (ts:space? s))) - (error (string "expected \"" closer "\" or separator in arguments to \"" - (if (eqv? closer #\]) #\[ #\{) " " closer - "\"; got \"" - (deparse (car vec)) t "\""))) - (loop (cons (parse-eq* s) vec) outer)))))))) + (parse-array-inner s (list (list first)) '() 0 0 closer gotnewline #f))) (define (expect-space-before s t) (if (not (ts:space? s)) @@ -1929,9 +1994,9 @@ (take-token s) (if (memv (peek-token s) (list #\, closer)) (parse-vect s first closer) - (parse-matrix s first closer #t last-end-symbol))) + (parse-array s first closer #t last-end-symbol))) (else - (parse-matrix s first closer #f last-end-symbol))))))) + (parse-array s first closer #f last-end-symbol))))))) (define (kw-to-= e) (if (kwarg? e) (cons '= (cdr e)) e)) (define (=-to-kw e) (if (assignment? e) (cons 'kw (cdr e)) e)) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index ca274ef552f5b..6bc8401c9b3dd 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1599,7 +1599,7 @@ ,(expand-update-operator op op= (car e) rhs T)))) (else (if (and (pair? lhs) (eq? op= '=) - (not (memq (car lhs) '(|.| tuple vcat typed_hcat typed_vcat)))) + (not (memq (car lhs) '(|.| tuple vcat ncat typed_hcat typed_vcat typed_ncat)))) (error (string "invalid assignment location \"" (deparse lhs) "\""))) (expand-update-operator- op op= lhs rhs declT)))) @@ -1976,6 +1976,113 @@ ,@(apply append rows)))) `(call ,@vcat ,@a)))))) +(define (expand-ncat e (hvncat '((top hvncat)))) + (define (is-row a) (and (pair? a) + (or (eq? (car a) 'row) + (eq? (car a) 'nrow)))) + (define (is-1d a) (not (any is-row a))) + (define (sum xs) (foldl + 0 xs)) + (define (get-shape a is-row-first d) + (define (zip xss) (apply map list xss)) + (define (get-next x) + (cond ((or (not (is-row x)) + (and (eq? (car x) 'nrow) (> d (1+ (cadr x)))) + (and (eq? (car x) 'row) (> d 1))) + (list x)) + ((eq? (car x) 'nrow) (cddr x)) + (else (cdr x)))) + ; describe the shape of the concatenation + (cond ((or (= d 0) + (and (not is-row-first) (= d 1))) + (length a)) + ((and is-row-first (= d 3)) + (get-shape a is-row-first (1- d))) + (else + (let ((ashape + (map (lambda (x) + (get-shape (get-next x) is-row-first (1- d))) + a))) + (if (pair? (car ashape)) + (let ((zipashape (zip ashape))) + (cons (sum (car zipashape)) + (cons (car zipashape) + (map (lambda (x) + (apply append x)) + (cdr zipashape))))) + (list (sum ashape) ashape)))))) + (define (get-dims a is-row-first d) + (cond ((and (< d 2) (not (is-row (car a)))) + (list (length a))) + ((= d 1) + (list (car (get-dims (cdar a) is-row-first 0)) (length a))) + ((and (= d 3) is-row-first) + (get-dims a is-row-first 2)) + (else + (let ((anext (if (and (pair? (car a)) + (eq? (caar a) 'nrow) + (= d (1+ (cadar a)))) + (cddar a) + (list (car a))))) + (cons (length a) (get-dims anext is-row-first (1- d))))))) + (define (is-balanced s) + ; determine whether there are exactly the same number of elements along each axis + (= 0 (sum (map (lambda (x y) + (sum (map (lambda (z) + (- z y)) + x))) + (cdr s) (map car (cdr s)))))) + (define (hasrows-flatten a) + ; (car ) stores if a row was observed + (foldl (lambda (x y) + (let ((r (car y)) + (yt (cdr y))) + (if (is-row x) + (if (eq? (car x) 'nrow) + (let* ((raflat (append (hasrows-flatten (cddr x)))) + (aflat (cdr raflat)) + (rinner (car raflat)) + (r (if (null? (or r rinner)) + (and r rinner) + r))) + (if (and (not (null? r)) + (or (null? rinner) (and (not r) rinner)) + (and (= (cadr x) 2) r)) + (error "cannot mix space and ;; separators in an array expression, except to wrap a line")) + (cons (if (and (= (cadr x) 2) (null? r)) + #f + r) + (append aflat yt))) + (if (or (null? r) r) + (cons #t (append (reverse (cdr x)) yt)) + (error "cannot mix space and ;; separators in an array expression, except to wrap a line"))) + (cons r (cons x yt))))) + (list '()) a)) + (define (tf a) (if a '(true) '(false))) + (define (tuplize s) + (cons 'tuple (reverse (map (lambda (x) + (cons 'tuple x)) + (cons (list (car s)) (cdr s)))))) + (let* ((d (cadr e)) + (a (cddr e)) + (raflat (hasrows-flatten a)) + (r (car raflat)) + (is-row-first (if (null? r) #f r)) + (aflat (reverse (cdr raflat)))) + (if (any assignment? aflat) + (error (string "misplaced assignment statement in \"" (deparse e) "\""))) + (if (has-parameters? aflat) + (error "unexpected parameters in array expression")) + (expand-forms + (if (is-1d a) + `(call ,@hvncat ,d ,@aflat) + (if (any vararg? aflat) + (error (string "Splatting ... in an hvncat with multiple dimensions is not supported")) + (let ((shape (get-shape a is-row-first d))) + (if (is-balanced shape) + (let ((dims `(tuple ,@(reverse (get-dims a is-row-first d))))) + `(call ,@hvncat ,dims ,(tf is-row-first) ,@aflat)) + `(call ,@hvncat ,(tuplize shape) ,(tf is-row-first) ,@aflat)))))))) + (define (expand-property-destruct lhss x) (if (not (length= lhss 1)) (error (string "invalid assignment location \"" (deparse lhs) "\""))) @@ -2220,7 +2327,7 @@ (expand-tuple-destruct lhss x)))) ((typed_hcat) (error "invalid spacing in left side of indexed assignment")) - ((typed_vcat) + ((typed_vcat typed_ncat) (error "unexpected \";\" in left side of indexed assignment")) ((ref) ;; (= (ref a . idxs) rhs) @@ -2257,7 +2364,7 @@ `(block ,@(cdr e) (decl ,(car e) ,T) (= ,(car e) ,rhs)))))) - ((vcat) + ((vcat ncat) ;; (= (vcat . args) rhs) (error "use \"(a, b) = ...\" to assign multiple values")) (else @@ -2493,6 +2600,8 @@ 'vcat expand-vcat + 'ncat expand-ncat + 'typed_hcat (lambda (e) (if (any assignment? (cddr e)) @@ -2505,6 +2614,12 @@ (e (cdr e))) (expand-vcat e `((top typed_vcat) ,t) `((top typed_hvcat) ,t) `((top typed_hvcat_rows) ,t)))) + 'typed_ncat + (lambda (e) + (let ((t (cadr e)) + (e (cdr e))) + (expand-ncat e `((top typed_hvncat) ,t)))) + '|'| (lambda (e) (expand-forms `(call |'| ,(cadr e)))) 'generator diff --git a/test/abstractarray.jl b/test/abstractarray.jl index 6c5ed26a1c289..05ec3efef1aab 100644 --- a/test/abstractarray.jl +++ b/test/abstractarray.jl @@ -1317,8 +1317,45 @@ end end end -@testset "reduce(vcat, ...) inferrence #40277" begin - x_vecs = ([5, ], [1.0, 2.0, 3.0]) - @test @inferred(reduce(vcat, x_vecs)) == [5.0, 1.0, 2.0, 3.0] - @test @inferred(reduce(vcat, ([10.0], [20.0], Bool[]))) == [10.0, 20.0] +@testset "hvncat" begin + a = fill(1, (2,3,2,4,5)) + b = fill(2, (1,1,2,4,5)) + c = fill(3, (1,2,2,4,5)) + d = fill(4, (1,1,1,4,5)) + e = fill(5, (1,1,1,4,5)) + f = fill(6, (1,1,1,4,5)) + g = fill(7, (2,3,1,4,5)) + h = fill(8, (3,3,3,1,2)) + i = fill(9, (3,2,3,3,2)) + j = fill(10, (3,1,3,3,2)) + + result = [a; b c ;;; d e f ; g ;;;;; h ;;;; i j] + @test size(result) == (3,3,3,4,7) + @test result == [a; [b ;; c] ;;; [d e f] ; g ;;;;; h ;;;; i ;; j] + @test result == cat(cat([a ; b c], [d e f ; g], dims = 3), cat(h, [i j], dims = 4), dims = 5) + + # terminating semicolons extend dimensions + @test [1;] == [1] + @test [1;;] == fill(1, (1,1)) + + for v in (1, fill(1), fill(1,1,1), fill(1, 1, 1, 1)) + @test_throws ArgumentError [v; v;; v] + @test_throws ArgumentError [v; v;; v; v; v] + @test_throws ArgumentError [v; v; v;; v; v] + @test_throws ArgumentError [v; v;; v; v;;; v; v;; v; v;; v; v] + @test_throws ArgumentError [v; v;; v; v;;; v; v] + @test_throws ArgumentError [v; v;; v; v;;; v; v; v;; v; v] + @test_throws ArgumentError [v; v;; v; v;;; v; v;; v; v; v] + # ensure a wrong shape with the right number of elements doesn't pass through + @test_throws ArgumentError [v; v;; v; v;;; v; v; v; v] + + @test [v; v;; v; v] == fill(1, ndims(v) == 3 ? (2, 2, 1) : (2,2)) + @test [v; v;; v; v;;;] == fill(1, 2, 2, 1) + @test [v; v;; v; v] == fill(1, ndims(v) == 3 ? (2, 2, 1) : (2,2)) + @test [v v; v v;;;] == fill(1, 2, 2, 1) + @test [v; v;; v; v;;; v; v;; v; v;;] == fill(1, 2, 2, 2) + @test [v; v; v;; v; v; v;;; v; v; v;; v; v; v;;] == fill(1, 3, 2, 2) + @test [v v; v v;;; v v; v v] == fill(1, 2, 2, 2) + @test [v v v; v v v;;; v v v; v v v] == fill(1, 2, 3, 2) + end end diff --git a/test/offsetarray.jl b/test/offsetarray.jl index 96a44c71e5483..58a683ac90c57 100644 --- a/test/offsetarray.jl +++ b/test/offsetarray.jl @@ -231,9 +231,9 @@ targets1 = ["0-dimensional OffsetArray(::Array{Float64, 0}) with eltype Float64: "1×1×1×1 OffsetArray(::Array{Float64, 4}, 2:2, 3:3, 4:4, 5:5) with eltype Float64 with indices 2:2×3:3×4:4×5:5:\n[:, :, 4, 5] =\n 1.0"] targets2 = ["(fill(1.0), fill(1.0))", "([1.0], [1.0])", - "([1.0], [1.0])", - "([1.0], [1.0])", - "([1.0], [1.0])"] + "([1.0;;], [1.0;;])", + "([1.0;;;], [1.0;;;])", + "([1.0;;;;], [1.0;;;;])"] @testset "printing of OffsetArray with n=$n" for n = 0:4 a = OffsetArray(fill(1.,ntuple(Returns(1),n)), ntuple(identity,n)) show(IOContext(io, :limit => true), MIME("text/plain"), a) diff --git a/test/show.jl b/test/show.jl index 2dde201b06dfb..358b792de105b 100644 --- a/test/show.jl +++ b/test/show.jl @@ -2286,3 +2286,14 @@ end s = sprint(show, MIME("text/plain"), Function) @test s == "Function" end + +@testset "printing inline n-dimensional arrays and one-column matrices" begin + @test replstr([Int[1 2 3 ;;; 4 5 6]]) == "1-element Vector{Array{$Int, 3}}:\n [1 2 3;;; 4 5 6]" + @test replstr([Int[1 2 3 ;;; 4 5 6;;;;]]) == "1-element Vector{Array{$Int, 4}}:\n [1 2 3;;; 4 5 6;;;;]" + @test replstr([fill(1, (20,20,20))]) == "1-element Vector{Array{$Int, 3}}:\n [1 1 … 1 1; 1 1 … 1 1; … ; 1 1 … 1 1; 1 1 … 1 1;;; 1 1 … 1 1; 1 1 … 1 1; … ; 1 1 … 1 1; 1 1 … 1 1;;; 1 1 … 1 1; 1 1 … 1 1; … ; 1 1 … 1 1; 1 1 … 1 1;;; … ;;; 1 1 … 1 1; 1 1 … 1 1; … ; 1 1 … 1 1; 1 1 … 1 1;;; 1 1 … 1 1; 1 1 … 1 1; … ; 1 1 … 1 1; 1 1 … 1 1;;; 1 1 … 1 1; 1 1 … 1 1; … ; 1 1 … 1 1; 1 1 … 1 1]" + @test replstr([fill(1, 5, 1)]) == "1-element Vector{Matrix{$Int}}:\n [1; 1; … ; 1; 1;;]" + @test replstr([fill(1, 5, 2)]) == "1-element Vector{Matrix{$Int}}:\n [1 1; 1 1; … ; 1 1; 1 1]" + @test replstr([[1;]]) == "1-element Vector{Vector{$Int}}:\n [1]" + @test replstr([[1;;]]) == "1-element Vector{Matrix{$Int}}:\n [1;;]" + @test replstr([[1;;;]]) == "1-element Vector{Array{$Int, 3}}:\n [1;;;]" +end diff --git a/test/syntax.jl b/test/syntax.jl index d934e9358baac..448595ff1a8d7 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -2699,6 +2699,26 @@ end @test Meta.isexpr(Meta.@lower(f((; a, b::Int)) = a + b), :error) end +# #33697 +@testset "N-dimensional concatenation" begin + @test :([1 2 5; 3 4 6;;; 0 9 3; 4 5 4]) == + Expr(:ncat, 3, Expr(:nrow, 1, Expr(:row, 1, 2, 5), Expr(:row, 3, 4, 6)), + Expr(:nrow, 1, Expr(:row, 0, 9, 3), Expr(:row, 4, 5, 4))) + @test :([1 ; 2 ;; 3 ; 4]) == Expr(:ncat, 2, Expr(:nrow, 1, 1, 2), Expr(:nrow, 1, 3, 4)) + + @test_throws ParseError Meta.parse("[1 2 ;; 3 4]") # cannot mix spaces and ;; except as line break + @test :([1 2 ;; + 3 4]) == :([1 2 3 4]) + @test :([1 2 ;; + 3 4 ; 2 3 4 5]) == :([1 2 3 4 ; 2 3 4 5]) + + @test Meta.parse("[1;\n]") == :([1;]) # ensure line breaks following semicolons are treated correctly + @test Meta.parse("[1;\n\n]") == :([1;]) + @test Meta.parse("[1\n;]") == :([1;]) # semicolons following a linebreak are fine + @test Meta.parse("[1\n;;; 2]") == :([1;;; 2]) + @test_throws ParseError Meta.parse("[1;\n;2]") # semicolons cannot straddle a line break +end + # issue #25652 x25652 = 1 x25652_2 = let (x25652, _) = (x25652, nothing)