Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow slurping in any position #42902

Merged
merged 19 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to use either indexing or SubString for both values?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my reasoning was that the type of last_n doesn't really matter as long as it iterates correctly, so we might as well avoid the extra allocation. I guess we could make front a SubString as well, but that might be wasteful for small string if n is not much smaller than the string length, since we can't free the original string

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()
simeonschaub marked this conversation as resolved.
Show resolved Hide resolved
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
139 changes: 94 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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, would be clearer to make this a mutable variable than a 1-element list.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, it is also passed to destructure- 😭

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agree it's a little bit awkward, but couldn't think of something more elegant

(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,49 @@
lhss)
(unnecessary ,xx))))

(define (destructure- i lhss xx n st end)
simeonschaub marked this conversation as resolved.
Show resolved Hide resolved
(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 +2290,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