Skip to content

Commit 385762b

Browse files
authored
allow slurping in any position (#42902)
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`.
1 parent 8890aea commit 385762b

File tree

9 files changed

+304
-50
lines changed

9 files changed

+304
-50
lines changed

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ New language features
55
---------------------
66

77
* It is now possible to assign to bindings in another module using `setproperty!(::Module, ::Symbol, x)`. ([#44137])
8+
* Slurping in assignments is now also allowed in non-final position. This is
9+
handled via `Base.split_rest`. ([#42902])
810

911
Language changes
1012
----------------

base/bitarray.jl

+7
Original file line numberDiff line numberDiff line change
@@ -1913,3 +1913,10 @@ function read!(s::IO, B::BitArray)
19131913
end
19141914

19151915
sizeof(B::BitArray) = sizeof(B.chunks)
1916+
1917+
function _split_rest(a::Union{Vector, BitVector}, n::Int)
1918+
_check_length_split_rest(length(a), n)
1919+
last_n = a[end-n+1:end]
1920+
resize!(a, length(a) - n)
1921+
return a, last_n
1922+
end

base/namedtuple.jl

+6
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,9 @@ macro NamedTuple(ex)
415415
types = [esc(e isa Symbol ? :Any : e.args[2]) for e in decls]
416416
return :(NamedTuple{($(vars...),), Tuple{$(types...)}})
417417
end
418+
419+
function split_rest(t::NamedTuple{names}, n::Int, st...) where {names}
420+
_check_length_split_rest(length(t), n)
421+
names_front, names_last_n = split_rest(names, n, st...)
422+
return NamedTuple{names_front}(t), NamedTuple{names_last_n}(t)
423+
end

base/strings/basic.jl

+13
Original file line numberDiff line numberDiff line change
@@ -780,3 +780,16 @@ julia> codeunits("Juλia")
780780
```
781781
"""
782782
codeunits(s::AbstractString) = CodeUnits(s)
783+
784+
function _split_rest(s::AbstractString, n::Int)
785+
lastind = lastindex(s)
786+
i = try
787+
prevind(s, lastind, n)
788+
catch e
789+
e isa BoundsError || rethrow()
790+
_check_length_split_rest(length(s), n)
791+
end
792+
last_n = SubString(s, nextind(s, i), lastind)
793+
front = s[begin:i]
794+
return front, last_n
795+
end

base/tuple.jl

+54-2
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,12 @@ if `collection` is an `AbstractString`, and an arbitrary iterator, falling back
108108
`Iterators.rest(collection[, itr_state])`, otherwise.
109109
110110
Can be overloaded for user-defined collection types to customize the behavior of [slurping
111-
in assignments](@ref destructuring-assignment), like `a, b... = collection`.
111+
in assignments](@ref destructuring-assignment) in final position, like `a, b... = collection`.
112112
113113
!!! compat "Julia 1.6"
114114
`Base.rest` requires at least Julia 1.6.
115115
116-
See also: [`first`](@ref first), [`Iterators.rest`](@ref).
116+
See also: [`first`](@ref first), [`Iterators.rest`](@ref), [`Base.split_rest`](@ref).
117117
118118
# Examples
119119
```jldoctest
@@ -136,6 +136,58 @@ rest(a::Array, i::Int=1) = a[i:end]
136136
rest(a::Core.SimpleVector, i::Int=1) = a[i:end]
137137
rest(itr, state...) = Iterators.rest(itr, state...)
138138

139+
"""
140+
Base.split_rest(collection, n::Int[, itr_state]) -> (rest_but_n, last_n)
141+
142+
Generic function for splitting the tail of `collection`, starting from a specific iteration
143+
state `itr_state`. Returns a tuple of two new collections. The first one contains all
144+
elements of the tail but the `n` last ones, which make up the second collection.
145+
146+
The type of the first collection generally follows that of [`Base.rest`](@ref), except that
147+
the fallback case is not lazy, but is collected eagerly into a vector.
148+
149+
Can be overloaded for user-defined collection types to customize the behavior of [slurping
150+
in assignments](@ref destructuring-assignment) in non-final position, like `a, b..., c = collection`.
151+
152+
!!! compat "Julia 1.9"
153+
`Base.split_rest` requires at least Julia 1.9.
154+
155+
See also: [`Base.rest`](@ref).
156+
157+
# Examples
158+
```jldoctest
159+
julia> a = [1 2; 3 4]
160+
2×2 Matrix{Int64}:
161+
1 2
162+
3 4
163+
164+
julia> first, state = iterate(a)
165+
(1, 2)
166+
167+
julia> first, Base.split_rest(a, 1, state)
168+
(1, ([3, 2], [4]))
169+
```
170+
"""
171+
function split_rest end
172+
function split_rest(itr, n::Int, state...)
173+
if IteratorSize(itr) == IsInfinite()
174+
throw(ArgumentError("Cannot split an infinite iterator in the middle."))
175+
end
176+
return _split_rest(rest(itr, state...), n)
177+
end
178+
_split_rest(itr, n::Int) = _split_rest(collect(itr), n)
179+
function _check_length_split_rest(len, n)
180+
len < n && throw(ArgumentError(
181+
"The iterator only contains $len elements, but at least $n were requested."
182+
))
183+
end
184+
function _split_rest(a::Union{AbstractArray, Core.SimpleVector}, n::Int)
185+
_check_length_split_rest(length(a), n)
186+
return a[begin:end-n], a[end-n+1:end]
187+
end
188+
189+
split_rest(t::Tuple, n::Int, i=1) = t[i:end-n], t[end-n+1:end]
190+
139191
# Use dispatch to avoid a branch in first
140192
first(::Tuple{}) = throw(ArgumentError("tuple must be non-empty"))
141193
first(t::Tuple) = t[1]

doc/src/base/collections.md

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ Base.replace(::Any, ::Pair...)
140140
Base.replace(::Base.Callable, ::Any)
141141
Base.replace!
142142
Base.rest
143+
Base.split_rest
143144
```
144145

145146
## Indexable Collections

doc/src/manual/functions.md

+52-1
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,57 @@ Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.
475475

476476
See [`Base.rest`](@ref) for details on the precise handling and customization for specific iterators.
477477

478+
!!! compat "Julia 1.9"
479+
`...` in non-final position of an assignment requires Julia 1.9
480+
481+
Slurping in assignments can also occur in any other position. As opposed to slurping the end
482+
of a collection however, this will always be eager.
483+
484+
```jldoctest
485+
julia> a, b..., c = 1:5
486+
1:5
487+
488+
julia> a
489+
1
490+
491+
julia> b
492+
3-element Vector{Int64}:
493+
2
494+
3
495+
4
496+
497+
julia> c
498+
5
499+
500+
julia> front..., tail = "Hi!"
501+
"Hi!"
502+
503+
julia> front
504+
"Hi"
505+
506+
julia> tail
507+
'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)
508+
```
509+
510+
This is implemented in terms of the function [`Base.split_rest`](@ref).
511+
512+
Note that for variadic function definitions, slurping is still only allowed in final position.
513+
This does not apply to [single argument destructuring](@ref man-argument-destructuring) though,
514+
as that does not affect method dispatch:
515+
516+
```jldoctest
517+
julia> f(x..., y) = x
518+
ERROR: syntax: invalid "..." on non-final argument
519+
Stacktrace:
520+
[...]
521+
522+
julia> f((x..., y)) = x
523+
f (generic function with 1 method)
524+
525+
julia> f((1, 2, 3))
526+
(1, 2)
527+
```
528+
478529
## Property destructuring
479530

480531
Instead of destructuring based on iteration, the right side of assignments can also be destructured using property names.
@@ -492,7 +543,7 @@ julia> b
492543
2
493544
```
494545

495-
## Argument destructuring
546+
## [Argument destructuring](@id man-argument-destructuring)
496547

497548
The destructuring feature can also be used within a function argument.
498549
If a function argument name is written as a tuple (e.g. `(x, y)`) instead of just

src/julia-syntax.scm

+104-45
Original file line numberDiff line numberDiff line change
@@ -1506,15 +1506,59 @@
15061506
after
15071507
(cons R elts)))
15081508
((vararg? L)
1509+
(if (any vararg? (cdr lhss))
1510+
(error "multiple \"...\" on lhs of assignment"))
15091511
(if (null? (cdr lhss))
15101512
(let ((temp (if (eventually-call? (cadr L)) (gensy) (make-ssavalue))))
15111513
`(block ,@(reverse stmts)
15121514
(= ,temp (tuple ,@rhss))
15131515
,@(reverse after)
15141516
(= ,(cadr L) ,temp)
15151517
(unnecessary (tuple ,@(reverse elts) (... ,temp)))))
1516-
(error (string "invalid \"...\" on non-final assignment location \""
1517-
(cadr L) "\""))))
1518+
(let ((lhss- (reverse lhss))
1519+
(rhss- (reverse rhss))
1520+
(lhs-tail '())
1521+
(rhs-tail '()))
1522+
(define (extract-tail)
1523+
(if (not (or (null? lhss-) (null? rhss-)
1524+
(vararg? (car lhss-)) (vararg? (car rhss-))))
1525+
(begin
1526+
(set! lhs-tail (cons (car lhss-) lhs-tail))
1527+
(set! rhs-tail (cons (car rhss-) rhs-tail))
1528+
(set! lhss- (cdr lhss-))
1529+
(set! rhss- (cdr rhss-))
1530+
(extract-tail))))
1531+
(extract-tail)
1532+
(let* ((temp (if (any (lambda (x)
1533+
(or (eventually-call? x)
1534+
(and (vararg? x) (eventually-call? (cadr x)))))
1535+
lhss-)
1536+
(gensy)
1537+
(make-ssavalue)))
1538+
(assigns (make-assignment temp `(tuple ,@(reverse rhss-))))
1539+
(assigns (if (symbol? temp)
1540+
`((local-def ,temp) ,assigns)
1541+
(list assigns)))
1542+
(n (length lhss-))
1543+
(st (gensy))
1544+
(end (list after))
1545+
(assigns (if (and (length= lhss- 1) (vararg? (car lhss-)))
1546+
(begin
1547+
(set-car! end
1548+
(cons `(= ,(cadar lhss-) ,temp) (car end)))
1549+
assigns)
1550+
(append (if (> n 0)
1551+
`(,@assigns (local ,st))
1552+
assigns)
1553+
(destructure- 1 (reverse lhss-) temp
1554+
n st end)))))
1555+
(loop lhs-tail
1556+
(append (map (lambda (x) (if (vararg? x) (cadr x) x)) lhss-) assigned)
1557+
rhs-tail
1558+
(append (reverse assigns) stmts)
1559+
(car end)
1560+
(cons `(... ,temp) elts))))))
1561+
15181562
((vararg? R)
15191563
(let ((temp (make-ssavalue)))
15201564
`(block ,@(reverse stmts)
@@ -2187,6 +2231,59 @@
21872231
lhss)
21882232
(unnecessary ,xx))))
21892233

2234+
;; implement tuple destructuring, possibly with slurping
2235+
;;
2236+
;; `i`: index of the current lhs arg
2237+
;; `lhss`: remaining lhs args
2238+
;; `xx`: the rhs, already either an ssavalue or something simple
2239+
;; `st`: empty list if i=1, otherwise contains the iteration state
2240+
;; `n`: total nr of lhs args
2241+
;; `end`: car collects statements to be executed afterwards.
2242+
;; In general, actual assignments should only happen after
2243+
;; the whole iterater is desctructured (https://github.com/JuliaLang/julia/issues/40574)
2244+
(define (destructure- i lhss xx n st end)
2245+
(if (null? lhss)
2246+
'()
2247+
(let* ((lhs (car lhss))
2248+
(lhs- (cond ((or (symbol? lhs) (ssavalue? lhs))
2249+
lhs)
2250+
((vararg? lhs)
2251+
(let ((lhs- (cadr lhs)))
2252+
(if (or (symbol? lhs-) (ssavalue? lhs-))
2253+
lhs
2254+
`(|...| ,(if (eventually-call? lhs-)
2255+
(gensy)
2256+
(make-ssavalue))))))
2257+
;; can't use ssavalues if it's a function definition
2258+
((eventually-call? lhs) (gensy))
2259+
(else (make-ssavalue)))))
2260+
(if (and (vararg? lhs) (any vararg? (cdr lhss)))
2261+
(error "multiple \"...\" on lhs of assignment"))
2262+
(if (not (eq? lhs lhs-))
2263+
(if (vararg? lhs)
2264+
(set-car! end (cons (expand-forms `(= ,(cadr lhs) ,(cadr lhs-))) (car end)))
2265+
(set-car! end (cons (expand-forms `(= ,lhs ,lhs-)) (car end)))))
2266+
(if (vararg? lhs-)
2267+
(if (= i n)
2268+
(if (underscore-symbol? (cadr lhs-))
2269+
'()
2270+
(list (expand-forms
2271+
`(= ,(cadr lhs-) (call (top rest) ,xx ,@(if (eq? i 1) '() `(,st)))))))
2272+
(let ((tail (if (eventually-call? lhs) (gensy) (make-ssavalue))))
2273+
(cons (expand-forms
2274+
(lower-tuple-assignment
2275+
(list (cadr lhs-) tail)
2276+
`(call (top split_rest) ,xx ,(- n i) ,@(if (eq? i 1) '() `(,st)))))
2277+
(destructure- 1 (cdr lhss) tail (- n i) st end))))
2278+
(cons (expand-forms
2279+
(lower-tuple-assignment
2280+
(if (= i n)
2281+
(list lhs-)
2282+
(list lhs- st))
2283+
`(call (top indexed_iterate)
2284+
,xx ,i ,@(if (eq? i 1) '() `(,st)))))
2285+
(destructure- (+ i 1) (cdr lhss) xx n st end))))))
2286+
21902287
(define (expand-tuple-destruct lhss x)
21912288
(define (sides-match? l r)
21922289
;; l and r either have equal lengths, or r has a trailing ...
@@ -2203,64 +2300,26 @@
22032300
(tuple-to-assignments lhss x))
22042301
;; (a, b, ...) = other
22052302
(begin
2206-
;; like memq, but if last element of lhss is (... sym),
2207-
;; check against sym instead
2303+
;; like memq, but if lhs is (... sym), check against sym instead
22082304
(define (in-lhs? x lhss)
22092305
(if (null? lhss)
22102306
#f
22112307
(let ((l (car lhss)))
22122308
(cond ((and (pair? l) (eq? (car l) '|...|))
2213-
(if (null? (cdr lhss))
2214-
(eq? (cadr l) x)
2215-
(error (string "invalid \"...\" on non-final assignment location \""
2216-
(cadr l) "\""))))
2309+
(eq? (cadr l) x))
22172310
((eq? l x) #t)
22182311
(else (in-lhs? x (cdr lhss)))))))
22192312
;; in-lhs? also checks for invalid syntax, so always call it first
22202313
(let* ((xx (maybe-ssavalue lhss x in-lhs?))
22212314
(ini (if (eq? x xx) '() (list (sink-assignment xx (expand-forms x)))))
22222315
(n (length lhss))
2223-
;; skip last assignment if it is an all-underscore vararg
2224-
(n (if (> n 0)
2225-
(let ((l (last lhss)))
2226-
(if (and (vararg? l) (underscore-symbol? (cadr l)))
2227-
(- n 1)
2228-
n))
2229-
n))
22302316
(st (gensy))
2231-
(end '()))
2317+
(end (list (list))))
22322318
`(block
22332319
,@(if (> n 0) `((local ,st)) '())
22342320
,@ini
2235-
,@(map (lambda (i lhs)
2236-
(let ((lhs- (cond ((or (symbol? lhs) (ssavalue? lhs))
2237-
lhs)
2238-
((vararg? lhs)
2239-
(let ((lhs- (cadr lhs)))
2240-
(if (or (symbol? lhs-) (ssavalue? lhs-))
2241-
lhs
2242-
`(|...| ,(if (eventually-call? lhs-)
2243-
(gensy)
2244-
(make-ssavalue))))))
2245-
;; can't use ssavalues if it's a function definition
2246-
((eventually-call? lhs) (gensy))
2247-
(else (make-ssavalue)))))
2248-
(if (not (eq? lhs lhs-))
2249-
(if (vararg? lhs)
2250-
(set! end (cons (expand-forms `(= ,(cadr lhs) ,(cadr lhs-))) end))
2251-
(set! end (cons (expand-forms `(= ,lhs ,lhs-)) end))))
2252-
(expand-forms
2253-
(if (vararg? lhs-)
2254-
`(= ,(cadr lhs-) (call (top rest) ,xx ,@(if (eq? i 0) '() `(,st))))
2255-
(lower-tuple-assignment
2256-
(if (= i (- n 1))
2257-
(list lhs-)
2258-
(list lhs- st))
2259-
`(call (top indexed_iterate)
2260-
,xx ,(+ i 1) ,@(if (eq? i 0) '() `(,st))))))))
2261-
(iota n)
2262-
lhss)
2263-
,@(reverse end)
2321+
,@(destructure- 1 lhss xx n st end)
2322+
,@(reverse (car end))
22642323
(unnecessary ,xx))))))
22652324

22662325
;; move an assignment into the last statement of a block to keep more statements at top level

0 commit comments

Comments
 (0)