From fc075da278bb9638e2093c4be4bb495a1325ef0e Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Mon, 1 Nov 2021 21:21:54 -0400 Subject: [PATCH 01/12] RFC: allow slurping in any position This extends the current slurping syntax by allowing the slurping to not only occur at the end, but anywhere on the lhs. This allows syntax like `a, b..., c = x` to work as expected. The feature is implemented using a new function called `split_rest` (definitely open to better names), which takes as arguments the iterator, the number of trailing variables at the end as a `Val` and possibly a previous iteration state. It then spits out a vector containing all slurped arguments and a tuple with the n values that get assigned to the rest of the variables. The plan would be to customize this for different finite collection, so that the first argument won't always be a vector, but that has not been implemented yet. `split_rest` differs from `rest` of course in that it always needs to be eager, since the trailing values need to be known immediately. This is why the slurped part has to be a vector for most iterables, instead of a lazy iterator as is the case for `rest`. Mainly opening this to get some feedback on the proposed API here. --- base/array.jl | 30 +++++++++ base/tuple.jl | 3 + src/julia-syntax.scm | 145 +++++++++++++++++++++++++++++-------------- test/syntax.jl | 42 ++++++++++++- 4 files changed, 170 insertions(+), 50 deletions(-) diff --git a/base/array.jl b/base/array.jl index 18a13e89c41c2..b947d01bd671c 100644 --- a/base/array.jl +++ b/base/array.jl @@ -2672,3 +2672,33 @@ function intersect(v::AbstractVector, r::AbstractRange) return vectorfilter(T, _shrink_filter!(seen), common) end intersect(r::AbstractRange, v::AbstractVector) = intersect(v, r) + + +_collect_n(itr, ::Val{0}) = error() +_collect_n(itr, ::Val{0}, st) = ((), st) +function _collect_n(itr, ::Val{N}, st...) where {N} + tmp = iterate(itr, st...) + if tmp === nothing + error("Iterator does not contain enough elements for the given variables.") + end + first, st′ = tmp + tail, st′′ = _collect_n(itr, Val(N-1), st′) + return (first, tail...), st′′ +end + +function split_rest(itr, ::Val{N}, st...) where {N} + if IteratorSize(itr) == IsInfinite() + error("Can't split an infinite iterator in the middle.") + end + last_n, st′ = _collect_n(itr, Val(N), st...) + front = Vector{@default_eltype(itr)}() + while true + tmp = iterate(itr, st′) + tmp === nothing && break + xᵢ, st′ = tmp + push!(front, first(last_n)) + last_n = (tail(last_n)..., xᵢ) + end + return front, last_n +end + diff --git a/base/tuple.jl b/base/tuple.jl index 5db0e40b495d3..600db414ae34d 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -136,6 +136,9 @@ 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...) +split_rest(t::Tuple, ::Val{N}) where {N} = t[1:end-N], t[end-N+1:end] +split_rest(t::Tuple, ::Val{N}, st) where {N} = t[st: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/src/julia-syntax.scm b/src/julia-syntax.scm index 1b11b48765951..84aa79cf4affb 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1459,6 +1459,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) @@ -1466,8 +1468,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) @@ -2136,6 +2180,50 @@ (cdar lhss)) (unnecessary ,xx)))) +(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 (call (top Val) ,(- 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 ... @@ -2152,22 +2240,17 @@ (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 (cond ((or (and (not (in-lhs? x lhss)) (symbol? x)) - (ssavalue? x)) + (let* ((xx (cond ((or (ssavalue? x) + (and (symbol? x) (not (in-lhs? x lhss)))) x) ((and (pair? lhss) (vararg? (last lhss)) (eventually-call? (cadr (last lhss)))) @@ -2175,47 +2258,13 @@ (else (make-ssavalue)))) (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 beff019d72e80..37f62aa9f21ba 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -2618,8 +2618,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 @@ -3088,3 +3086,43 @@ function checkUserAccess(u::User) return false end """) + +@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 ErrorException 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] +end From d4cefef85267ce7f749c76007eeb1bd20e1b63f5 Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Mon, 10 Jan 2022 16:16:24 +0100 Subject: [PATCH 02/12] don't force tail to be static --- base/array.jl | 30 ------------------------------ base/bitarray.jl | 7 +++++++ base/namedtuple.jl | 6 ++++++ base/strings/basic.jl | 13 +++++++++++++ base/tuple.jl | 20 ++++++++++++++++++-- src/julia-syntax.scm | 3 +-- 6 files changed, 45 insertions(+), 34 deletions(-) diff --git a/base/array.jl b/base/array.jl index b947d01bd671c..18a13e89c41c2 100644 --- a/base/array.jl +++ b/base/array.jl @@ -2672,33 +2672,3 @@ function intersect(v::AbstractVector, r::AbstractRange) return vectorfilter(T, _shrink_filter!(seen), common) end intersect(r::AbstractRange, v::AbstractVector) = intersect(v, r) - - -_collect_n(itr, ::Val{0}) = error() -_collect_n(itr, ::Val{0}, st) = ((), st) -function _collect_n(itr, ::Val{N}, st...) where {N} - tmp = iterate(itr, st...) - if tmp === nothing - error("Iterator does not contain enough elements for the given variables.") - end - first, st′ = tmp - tail, st′′ = _collect_n(itr, Val(N-1), st′) - return (first, tail...), st′′ -end - -function split_rest(itr, ::Val{N}, st...) where {N} - if IteratorSize(itr) == IsInfinite() - error("Can't split an infinite iterator in the middle.") - end - last_n, st′ = _collect_n(itr, Val(N), st...) - front = Vector{@default_eltype(itr)}() - while true - tmp = iterate(itr, st′) - tmp === nothing && break - xᵢ, st′ = tmp - push!(front, first(last_n)) - last_n = (tail(last_n)..., xᵢ) - end - return front, last_n -end - 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 d05ad6e10b544..6ad364ba49a3e 100644 --- a/base/namedtuple.jl +++ b/base/namedtuple.jl @@ -406,3 +406,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 515b836311698..a62146abe7b74 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() + @assert _check_length_split_rest(length(s), n) + end + last_n = SubString(s, i, lastind) + front = s[begin:prevind(s, i)] + return front, last_n +end diff --git a/base/tuple.jl b/base/tuple.jl index 600db414ae34d..3c16d41f303ed 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -136,8 +136,24 @@ 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...) -split_rest(t::Tuple, ::Val{N}) where {N} = t[1:end-N], t[end-N+1:end] -split_rest(t::Tuple, ::Val{N}, st) where {N} = t[st:end-N], t[end-N+1: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")) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 84aa79cf4affb..e8fd0e33f6fae 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -2212,8 +2212,7 @@ (cons (expand-forms (lower-tuple-assignment (list (cadr lhs-) tail) - `(call (top split_rest) ,xx (call (top Val) ,(- n i)) - ,@(if (eq? i 1) '() `(,st))))) + `(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 From a1ada06dfa6ffef4bb9f4bc6eb395c027bf7f01d Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Tue, 11 Jan 2022 18:46:17 +0100 Subject: [PATCH 03/12] fix tests, add some new ones --- base/strings/basic.jl | 4 ++-- test/syntax.jl | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/base/strings/basic.jl b/base/strings/basic.jl index a62146abe7b74..135f015f4b8f2 100644 --- a/base/strings/basic.jl +++ b/base/strings/basic.jl @@ -789,7 +789,7 @@ function _split_rest(s::AbstractString, n::Int) e isa BoundsError || rethrow() @assert _check_length_split_rest(length(s), n) end - last_n = SubString(s, i, lastind) - front = s[begin:prevind(s, i)] + last_n = SubString(s, nextind(s, i), lastind) + front = s[begin:i] return front, last_n end diff --git a/test/syntax.jl b/test/syntax.jl index 37f62aa9f21ba..5d51d1e13bfbe 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -3118,11 +3118,36 @@ end @test y == (2:4,) @test z == 5 - @test_throws ErrorException begin x, y..., z = 1:1 end + @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 From 5ab22cd8dc2353da9c1fb5e7682ede1b7883026b Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Thu, 20 Jan 2022 19:21:36 +0100 Subject: [PATCH 04/12] add some docs --- base/tuple.jl | 37 ++++++++++++++++++++++++-- doc/src/manual/functions.md | 53 ++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/base/tuple.jl b/base/tuple.jl index dc538fcfd973a..1a8cf47301ead 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,39 @@ 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.8" + `Base.split_rest` requires at least Julia 1.8. + +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 rest end function split_rest(itr, n::Int, state...) if IteratorSize(itr) == IsInfinite() throw(ArgumentError("Cannot split an infinite iterator in the middle.")) diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index de20034cf7212..6cc981be14dba 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.8" + `...` in non-final position of an assignment requires Julia 1.8 + +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 From 9d1e5b7ae0bff4402607c6977d56311700061735 Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Thu, 20 Jan 2022 13:53:44 -0500 Subject: [PATCH 05/12] add NEWS entry --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index 3f23aaf573b72..90c7acd7c0899 100644 --- a/NEWS.md +++ b/NEWS.md @@ -20,6 +20,8 @@ New language features them after construction, providing for greater clarity and optimization ability of these objects ([#43305]). * Empty n-dimensional arrays can now be created using multiple semicolons inside square brackets, i.e. `[;;;]` creates a 0×0×0 `Array`. ([#41618]) +* Slurping in assignments is now also allowed in non-final position. This is + handled via `Base.split_rest`. ([#42902]) Language changes ---------------- @@ -76,6 +78,8 @@ New library functions * `hardlink(src, dst)` can be used to create hard links. ([#41639]) * `setcpuaffinity(cmd, cpus)` can be used to set CPU affinity of sub-processes. ([#42469]) * `diskstat(path=pwd())` can be used to return statistics about the disk. ([#42248]) +* `Base.split_rest` for splitting the tail of a collection from an optional iteration + state in two, with the latter being of length `n`. ([#42902]) New library features -------------------- From 6c2eedb79934ecf7a44a4e2806c4643d4a34afad Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Thu, 20 Jan 2022 15:45:03 -0500 Subject: [PATCH 06/12] fix docs --- base/tuple.jl | 2 +- doc/src/base/collections.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/base/tuple.jl b/base/tuple.jl index 1a8cf47301ead..40655f3bc8a66 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -168,7 +168,7 @@ julia> first, Base.split_rest(a, 1, state) (1, ([3, 2], [4])) ``` """ -function rest end +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.")) diff --git a/doc/src/base/collections.md b/doc/src/base/collections.md index d329ce6ef6119..7898625a35710 100644 --- a/doc/src/base/collections.md +++ b/doc/src/base/collections.md @@ -138,6 +138,7 @@ Base.replace(::Any, ::Pair...) Base.replace(::Base.Callable, ::Any) Base.replace! Base.rest +Base.split_rest ``` ## Indexable Collections From 0b438c4cff26455acf1269a3c1d7f3f1fbd7730f Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Thu, 20 Jan 2022 17:37:10 -0500 Subject: [PATCH 07/12] fix whitespace --- NEWS.md | 2 +- base/tuple.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 3200ee577541c..c96cbf0021537 100644 --- a/NEWS.md +++ b/NEWS.md @@ -79,7 +79,7 @@ New library functions * `setcpuaffinity(cmd, cpus)` can be used to set CPU affinity of sub-processes. ([#42469]) * `diskstat(path=pwd())` can be used to return statistics about the disk. ([#42248]) * `Base.split_rest` for splitting the tail of a collection from an optional iteration - state in two, with the latter being of length `n`. ([#42902]) + state in two, with the latter being of length `n`. ([#42902]) New library features -------------------- diff --git a/base/tuple.jl b/base/tuple.jl index 40655f3bc8a66..73429e9dad7b1 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -113,7 +113,7 @@ in assignments](@ref destructuring-assignment) in final position, like `a, b... !!! compat "Julia 1.6" `Base.rest` requires at least Julia 1.6. - See also: [`first`](@ref first), [`Iterators.rest`](@ref), [`Base.split_rest`](@ref). +See also: [`first`](@ref first), [`Iterators.rest`](@ref), [`Base.split_rest`](@ref). # Examples ```jldoctest From 0d9d04b162c9612519ba9fc38c76b60e8f5441cf Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Wed, 6 Apr 2022 17:25:24 -0400 Subject: [PATCH 08/12] update compat annotations --- base/tuple.jl | 4 ++-- doc/src/manual/functions.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/base/tuple.jl b/base/tuple.jl index 1c65be696caf7..e2b4d9ee745e6 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -149,8 +149,8 @@ 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.8" - `Base.split_rest` requires at least Julia 1.8. +!!! compat "Julia 1.9" + `Base.split_rest` requires at least Julia 1.9. See also: [`Base.rest`](@ref). diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index 2e75580712b2e..2724fa32ec382 100644 --- a/doc/src/manual/functions.md +++ b/doc/src/manual/functions.md @@ -475,8 +475,8 @@ 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.8" - `...` in non-final position of an assignment requires Julia 1.8 +!!! 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. From 34a24c325ff3b709ebdcec4e66035b4b9746414b Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Wed, 6 Apr 2022 17:26:15 -0400 Subject: [PATCH 09/12] remove nonsense assert --- base/strings/basic.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/strings/basic.jl b/base/strings/basic.jl index 2010255d13482..23e65ab4839a9 100644 --- a/base/strings/basic.jl +++ b/base/strings/basic.jl @@ -787,7 +787,7 @@ function _split_rest(s::AbstractString, n::Int) prevind(s, lastind, n) catch e e isa BoundsError || rethrow() - @assert _check_length_split_rest(length(s), n) + _check_length_split_rest(length(s), n) end last_n = SubString(s, nextind(s, i), lastind) front = s[begin:i] From 73c81553d93c0ad81377229f8934ceb00c55d779 Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Wed, 6 Apr 2022 17:28:28 -0400 Subject: [PATCH 10/12] fix bad rebase n NEWS [ci skip] --- NEWS.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2872843992df2..3735d72dc63d9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -36,16 +36,8 @@ Build system changes New library functions --------------------- -<<<<<<< HEAD -* `hardlink(src, dst)` can be used to create hard links. ([#41639]) -* `setcpuaffinity(cmd, cpus)` can be used to set CPU affinity of sub-processes. ([#42469]) -* `diskstat(path=pwd())` can be used to return statistics about the disk. ([#42248]) -* `Base.split_rest` for splitting the tail of a collection from an optional iteration - state in two, with the latter being of length `n`. ([#42902]) -======= Library changes --------------- ->>>>>>> origin/master * A known concurrency issue of `iterate` methods on `Dict` and other derived objects such as `keys(::Dict)`, `values(::Dict)`, and `Set` is fixed. These methods of `iterate` can From ae132392a83da0d6a0ed11f03b2d8769a2c64d57 Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Wed, 6 Apr 2022 17:31:04 -0400 Subject: [PATCH 11/12] retrigger CI From e3c106676506e96de3598e17235d967d606962f0 Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Fri, 8 Apr 2022 00:28:30 -0400 Subject: [PATCH 12/12] explain `destructure-` --- src/julia-syntax.scm | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 8df777db20ec3..74ce2a8359e82 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -2231,6 +2231,16 @@ 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) '()