Skip to content

Commit

Permalink
allow slurping in any position (#42902)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
simeonschaub authored Apr 8, 2022
1 parent 8890aea commit 385762b
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 50 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------
Expand Down
7 changes: 7 additions & 0 deletions base/bitarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions base/namedtuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions base/strings/basic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
56 changes: 54 additions & 2 deletions base/tuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
1 change: 1 addition & 0 deletions doc/src/base/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Base.replace(::Any, ::Pair...)
Base.replace(::Base.Callable, ::Any)
Base.replace!
Base.rest
Base.split_rest
```

## Indexable Collections
Expand Down
53 changes: 52 additions & 1 deletion doc/src/manual/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
149 changes: 104 additions & 45 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1506,15 +1506,59 @@
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)
(= ,temp (tuple ,@rhss))
,@(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)
Expand Down Expand Up @@ -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 ...
Expand All @@ -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
Expand Down
Loading

0 comments on commit 385762b

Please sign in to comment.