diff --git a/NEWS.md b/NEWS.md index 9a91317e4908e..f05ff20e078ed 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,8 @@ New language features --------------------- * It is now possible to assign to bindings in another module using `setproperty!(::Module, ::Symbol, x)`. ([#44137]) +* Slurping in assignments is now also allowed in non-final position. This is + handled via `Base.split_rest`. ([#42902]) Language changes ---------------- diff --git a/base/bitarray.jl b/base/bitarray.jl index 33e2715572018..4494218172bf1 100644 --- a/base/bitarray.jl +++ b/base/bitarray.jl @@ -1913,3 +1913,10 @@ function read!(s::IO, B::BitArray) end sizeof(B::BitArray) = sizeof(B.chunks) + +function _split_rest(a::Union{Vector, BitVector}, n::Int) + _check_length_split_rest(length(a), n) + last_n = a[end-n+1:end] + resize!(a, length(a) - n) + return a, last_n +end diff --git a/base/namedtuple.jl b/base/namedtuple.jl index 01fbeeec694e3..9282bdcc91e73 100644 --- a/base/namedtuple.jl +++ b/base/namedtuple.jl @@ -415,3 +415,9 @@ macro NamedTuple(ex) types = [esc(e isa Symbol ? :Any : e.args[2]) for e in decls] return :(NamedTuple{($(vars...),), Tuple{$(types...)}}) end + +function split_rest(t::NamedTuple{names}, n::Int, st...) where {names} + _check_length_split_rest(length(t), n) + names_front, names_last_n = split_rest(names, n, st...) + return NamedTuple{names_front}(t), NamedTuple{names_last_n}(t) +end diff --git a/base/strings/basic.jl b/base/strings/basic.jl index 45e5901d1ccec..23e65ab4839a9 100644 --- a/base/strings/basic.jl +++ b/base/strings/basic.jl @@ -780,3 +780,16 @@ julia> codeunits("Juλia") ``` """ codeunits(s::AbstractString) = CodeUnits(s) + +function _split_rest(s::AbstractString, n::Int) + lastind = lastindex(s) + i = try + prevind(s, lastind, n) + catch e + e isa BoundsError || rethrow() + _check_length_split_rest(length(s), n) + end + last_n = SubString(s, nextind(s, i), lastind) + front = s[begin:i] + return front, last_n +end diff --git a/base/tuple.jl b/base/tuple.jl index 3803763960c16..e2b4d9ee745e6 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -108,12 +108,12 @@ if `collection` is an `AbstractString`, and an arbitrary iterator, falling back `Iterators.rest(collection[, itr_state])`, otherwise. Can be overloaded for user-defined collection types to customize the behavior of [slurping -in assignments](@ref destructuring-assignment), like `a, b... = collection`. +in assignments](@ref destructuring-assignment) in final position, like `a, b... = collection`. !!! compat "Julia 1.6" `Base.rest` requires at least Julia 1.6. -See also: [`first`](@ref first), [`Iterators.rest`](@ref). +See also: [`first`](@ref first), [`Iterators.rest`](@ref), [`Base.split_rest`](@ref). # Examples ```jldoctest @@ -136,6 +136,58 @@ rest(a::Array, i::Int=1) = a[i:end] rest(a::Core.SimpleVector, i::Int=1) = a[i:end] rest(itr, state...) = Iterators.rest(itr, state...) +""" + Base.split_rest(collection, n::Int[, itr_state]) -> (rest_but_n, last_n) + +Generic function for splitting the tail of `collection`, starting from a specific iteration +state `itr_state`. Returns a tuple of two new collections. The first one contains all +elements of the tail but the `n` last ones, which make up the second collection. + +The type of the first collection generally follows that of [`Base.rest`](@ref), except that +the fallback case is not lazy, but is collected eagerly into a vector. + +Can be overloaded for user-defined collection types to customize the behavior of [slurping +in assignments](@ref destructuring-assignment) in non-final position, like `a, b..., c = collection`. + +!!! compat "Julia 1.9" + `Base.split_rest` requires at least Julia 1.9. + +See also: [`Base.rest`](@ref). + +# Examples +```jldoctest +julia> a = [1 2; 3 4] +2×2 Matrix{Int64}: + 1 2 + 3 4 + +julia> first, state = iterate(a) +(1, 2) + +julia> first, Base.split_rest(a, 1, state) +(1, ([3, 2], [4])) +``` +""" +function split_rest end +function split_rest(itr, n::Int, state...) + if IteratorSize(itr) == IsInfinite() + throw(ArgumentError("Cannot split an infinite iterator in the middle.")) + end + return _split_rest(rest(itr, state...), n) +end +_split_rest(itr, n::Int) = _split_rest(collect(itr), n) +function _check_length_split_rest(len, n) + len < n && throw(ArgumentError( + "The iterator only contains $len elements, but at least $n were requested." + )) +end +function _split_rest(a::Union{AbstractArray, Core.SimpleVector}, n::Int) + _check_length_split_rest(length(a), n) + return a[begin:end-n], a[end-n+1:end] +end + +split_rest(t::Tuple, n::Int, i=1) = t[i:end-n], t[end-n+1:end] + # Use dispatch to avoid a branch in first first(::Tuple{}) = throw(ArgumentError("tuple must be non-empty")) first(t::Tuple) = t[1] diff --git a/doc/src/base/collections.md b/doc/src/base/collections.md index 511ab786e158c..d096bf08e13ad 100644 --- a/doc/src/base/collections.md +++ b/doc/src/base/collections.md @@ -140,6 +140,7 @@ Base.replace(::Any, ::Pair...) Base.replace(::Base.Callable, ::Any) Base.replace! Base.rest +Base.split_rest ``` ## Indexable Collections diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index b0c70a378df89..2724fa32ec382 100644 --- a/doc/src/manual/functions.md +++ b/doc/src/manual/functions.md @@ -475,6 +475,57 @@ Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base. See [`Base.rest`](@ref) for details on the precise handling and customization for specific iterators. +!!! compat "Julia 1.9" + `...` in non-final position of an assignment requires Julia 1.9 + +Slurping in assignments can also occur in any other position. As opposed to slurping the end +of a collection however, this will always be eager. + +```jldoctest +julia> a, b..., c = 1:5 +1:5 + +julia> a +1 + +julia> b +3-element Vector{Int64}: + 2 + 3 + 4 + +julia> c +5 + +julia> front..., tail = "Hi!" +"Hi!" + +julia> front +"Hi" + +julia> tail +'!': ASCII/Unicode U+0021 (category Po: Punctuation, other) +``` + +This is implemented in terms of the function [`Base.split_rest`](@ref). + +Note that for variadic function definitions, slurping is still only allowed in final position. +This does not apply to [single argument destructuring](@ref man-argument-destructuring) though, +as that does not affect method dispatch: + +```jldoctest +julia> f(x..., y) = x +ERROR: syntax: invalid "..." on non-final argument +Stacktrace: +[...] + +julia> f((x..., y)) = x +f (generic function with 1 method) + +julia> f((1, 2, 3)) +(1, 2) +``` + ## Property destructuring Instead of destructuring based on iteration, the right side of assignments can also be destructured using property names. @@ -492,7 +543,7 @@ julia> b 2 ``` -## Argument destructuring +## [Argument destructuring](@id man-argument-destructuring) The destructuring feature can also be used within a function argument. If a function argument name is written as a tuple (e.g. `(x, y)`) instead of just diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 9bb6622209ae2..74ce2a8359e82 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1506,6 +1506,8 @@ after (cons R elts))) ((vararg? L) + (if (any vararg? (cdr lhss)) + (error "multiple \"...\" on lhs of assignment")) (if (null? (cdr lhss)) (let ((temp (if (eventually-call? (cadr L)) (gensy) (make-ssavalue)))) `(block ,@(reverse stmts) @@ -1513,8 +1515,50 @@ ,@(reverse after) (= ,(cadr L) ,temp) (unnecessary (tuple ,@(reverse elts) (... ,temp))))) - (error (string "invalid \"...\" on non-final assignment location \"" - (cadr L) "\"")))) + (let ((lhss- (reverse lhss)) + (rhss- (reverse rhss)) + (lhs-tail '()) + (rhs-tail '())) + (define (extract-tail) + (if (not (or (null? lhss-) (null? rhss-) + (vararg? (car lhss-)) (vararg? (car rhss-)))) + (begin + (set! lhs-tail (cons (car lhss-) lhs-tail)) + (set! rhs-tail (cons (car rhss-) rhs-tail)) + (set! lhss- (cdr lhss-)) + (set! rhss- (cdr rhss-)) + (extract-tail)))) + (extract-tail) + (let* ((temp (if (any (lambda (x) + (or (eventually-call? x) + (and (vararg? x) (eventually-call? (cadr x))))) + lhss-) + (gensy) + (make-ssavalue))) + (assigns (make-assignment temp `(tuple ,@(reverse rhss-)))) + (assigns (if (symbol? temp) + `((local-def ,temp) ,assigns) + (list assigns))) + (n (length lhss-)) + (st (gensy)) + (end (list after)) + (assigns (if (and (length= lhss- 1) (vararg? (car lhss-))) + (begin + (set-car! end + (cons `(= ,(cadar lhss-) ,temp) (car end))) + assigns) + (append (if (> n 0) + `(,@assigns (local ,st)) + assigns) + (destructure- 1 (reverse lhss-) temp + n st end))))) + (loop lhs-tail + (append (map (lambda (x) (if (vararg? x) (cadr x) x)) lhss-) assigned) + rhs-tail + (append (reverse assigns) stmts) + (car end) + (cons `(... ,temp) elts)))))) + ((vararg? R) (let ((temp (make-ssavalue))) `(block ,@(reverse stmts) @@ -2187,6 +2231,59 @@ lhss) (unnecessary ,xx)))) +;; implement tuple destructuring, possibly with slurping +;; +;; `i`: index of the current lhs arg +;; `lhss`: remaining lhs args +;; `xx`: the rhs, already either an ssavalue or something simple +;; `st`: empty list if i=1, otherwise contains the iteration state +;; `n`: total nr of lhs args +;; `end`: car collects statements to be executed afterwards. +;; In general, actual assignments should only happen after +;; the whole iterater is desctructured (https://github.com/JuliaLang/julia/issues/40574) +(define (destructure- i lhss xx n st end) + (if (null? lhss) + '() + (let* ((lhs (car lhss)) + (lhs- (cond ((or (symbol? lhs) (ssavalue? lhs)) + lhs) + ((vararg? lhs) + (let ((lhs- (cadr lhs))) + (if (or (symbol? lhs-) (ssavalue? lhs-)) + lhs + `(|...| ,(if (eventually-call? lhs-) + (gensy) + (make-ssavalue)))))) + ;; can't use ssavalues if it's a function definition + ((eventually-call? lhs) (gensy)) + (else (make-ssavalue))))) + (if (and (vararg? lhs) (any vararg? (cdr lhss))) + (error "multiple \"...\" on lhs of assignment")) + (if (not (eq? lhs lhs-)) + (if (vararg? lhs) + (set-car! end (cons (expand-forms `(= ,(cadr lhs) ,(cadr lhs-))) (car end))) + (set-car! end (cons (expand-forms `(= ,lhs ,lhs-)) (car end))))) + (if (vararg? lhs-) + (if (= i n) + (if (underscore-symbol? (cadr lhs-)) + '() + (list (expand-forms + `(= ,(cadr lhs-) (call (top rest) ,xx ,@(if (eq? i 1) '() `(,st))))))) + (let ((tail (if (eventually-call? lhs) (gensy) (make-ssavalue)))) + (cons (expand-forms + (lower-tuple-assignment + (list (cadr lhs-) tail) + `(call (top split_rest) ,xx ,(- n i) ,@(if (eq? i 1) '() `(,st))))) + (destructure- 1 (cdr lhss) tail (- n i) st end)))) + (cons (expand-forms + (lower-tuple-assignment + (if (= i n) + (list lhs-) + (list lhs- st)) + `(call (top indexed_iterate) + ,xx ,i ,@(if (eq? i 1) '() `(,st))))) + (destructure- (+ i 1) (cdr lhss) xx n st end)))))) + (define (expand-tuple-destruct lhss x) (define (sides-match? l r) ;; l and r either have equal lengths, or r has a trailing ... @@ -2203,64 +2300,26 @@ (tuple-to-assignments lhss x)) ;; (a, b, ...) = other (begin - ;; like memq, but if last element of lhss is (... sym), - ;; check against sym instead + ;; like memq, but if lhs is (... sym), check against sym instead (define (in-lhs? x lhss) (if (null? lhss) #f (let ((l (car lhss))) (cond ((and (pair? l) (eq? (car l) '|...|)) - (if (null? (cdr lhss)) - (eq? (cadr l) x) - (error (string "invalid \"...\" on non-final assignment location \"" - (cadr l) "\"")))) + (eq? (cadr l) x)) ((eq? l x) #t) (else (in-lhs? x (cdr lhss))))))) ;; in-lhs? also checks for invalid syntax, so always call it first (let* ((xx (maybe-ssavalue lhss x in-lhs?)) (ini (if (eq? x xx) '() (list (sink-assignment xx (expand-forms x))))) (n (length lhss)) - ;; skip last assignment if it is an all-underscore vararg - (n (if (> n 0) - (let ((l (last lhss))) - (if (and (vararg? l) (underscore-symbol? (cadr l))) - (- n 1) - n)) - n)) (st (gensy)) - (end '())) + (end (list (list)))) `(block ,@(if (> n 0) `((local ,st)) '()) ,@ini - ,@(map (lambda (i lhs) - (let ((lhs- (cond ((or (symbol? lhs) (ssavalue? lhs)) - lhs) - ((vararg? lhs) - (let ((lhs- (cadr lhs))) - (if (or (symbol? lhs-) (ssavalue? lhs-)) - lhs - `(|...| ,(if (eventually-call? lhs-) - (gensy) - (make-ssavalue)))))) - ;; can't use ssavalues if it's a function definition - ((eventually-call? lhs) (gensy)) - (else (make-ssavalue))))) - (if (not (eq? lhs lhs-)) - (if (vararg? lhs) - (set! end (cons (expand-forms `(= ,(cadr lhs) ,(cadr lhs-))) end)) - (set! end (cons (expand-forms `(= ,lhs ,lhs-)) end)))) - (expand-forms - (if (vararg? lhs-) - `(= ,(cadr lhs-) (call (top rest) ,xx ,@(if (eq? i 0) '() `(,st)))) - (lower-tuple-assignment - (if (= i (- n 1)) - (list lhs-) - (list lhs- st)) - `(call (top indexed_iterate) - ,xx ,(+ i 1) ,@(if (eq? i 0) '() `(,st)))))))) - (iota n) - lhss) - ,@(reverse end) + ,@(destructure- 1 lhss xx n st end) + ,@(reverse (car end)) (unnecessary ,xx)))))) ;; move an assignment into the last statement of a block to keep more statements at top level diff --git a/test/syntax.jl b/test/syntax.jl index 5fbb5c6c44963..36e4f0745bafc 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -2668,8 +2668,6 @@ end @test x == 1 && y == 2 @test z == (3:5,) - @test Meta.isexpr(Meta.@lower(begin a, b..., c = 1:3 end), :error) - @test Meta.isexpr(Meta.@lower(begin a, b..., c = 1, 2, 3 end), :error) @test Meta.isexpr(Meta.@lower(begin a, b..., c... = 1, 2, 3 end), :error) @test_throws BoundsError begin x, y, z... = 1:1 end @@ -3294,3 +3292,68 @@ end # issue 44723 demo44723()::Any = Base.Experimental.@opaque () -> true ? 1 : 2 @test demo44723()() == 1 + +@testset "slurping in non-final position" begin + res = begin x, y..., z = 1:7 end + @test res == 1:7 + @test x == 1 + @test y == Vector(2:6) + @test z == 7 + + res = begin x, y..., z = [1, 2] end + @test res == [1, 2] + @test x == 1 + @test y == Int[] + @test z == 2 + + x, y, z... = 1:7 + res = begin y, z..., x = z..., x, y end + @test res == ((3:7)..., 1, 2) + @test y == 3 + @test z == ((4:7)..., 1) + @test x == 2 + + res = begin x, _..., y = 1, 2 end + @test res == (1, 2) + @test x == 1 + @test y == 2 + + res = begin x, y..., z = 1, 2:4, 5 end + @test res == (1, 2:4, 5) + @test x == 1 + @test y == (2:4,) + @test z == 5 + + @test_throws ArgumentError begin x, y..., z = 1:1 end + @test_throws BoundsError begin x, y, _..., z = 1, 2 end + + last((a..., b)) = b + front((a..., b)) = a + @test last(1:3) == 3 + @test front(1:3) == [1, 2] + + res = begin x, y..., z = "abcde" end + @test res == "abcde" + @test x == 'a' + @test y == "bcd" + @test z == 'e' + + res = begin x, y..., z = (a=1, b=2, c=3, d=4) end + @test res == (a=1, b=2, c=3, d=4) + @test x == 1 + @test y == (b=2, c=3) + @test z == 4 + + v = rand(Bool, 7) + res = begin x, y..., z = v end + @test res === v + @test x == v[1] + @test y == v[2:6] + @test z == v[end] + + res = begin x, y..., z = Core.svec(1, 2, 3, 4) end + @test res == Core.svec(1, 2, 3, 4) + @test x == 1 + @test y == Core.svec(2, 3) + @test z == 4 +end