Skip to content

Commit

Permalink
implement property destructuring (#39285)
Browse files Browse the repository at this point in the history
* implement property destructuring

This currently only allows the form `(; a, b) = x` and lowers this to calls to `getproperty`. We could think about whether we want to allow specifying default values for properties as well, or even some kind of property renaming in the lhs. We could even allow slurping unused properties, but all of that sounds more difficult to work out in detail and potentially controversial, so I left this as an error for now.

fixes #28579

* add NEWS entry
  • Loading branch information
simeonschaub authored Jan 23, 2021
1 parent ae53238 commit d9e2632
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 59 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Julia v1.7 Release Notes
New language features
---------------------

* `(; a, b) = x` can now be used to destructure properties `a` and `b` of `x`. This syntax is equivalent to `a = getproperty(x, :a)`
and similarly for `b`. ([#39285])

Language changes
----------------

Expand Down
135 changes: 76 additions & 59 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,66 @@
,@(map expand-forms (cddr e))))
(cons (car e) (map expand-forms (cdr e))))))

(define (expand-tuple-destruct lhss x)
(define (sides-match? l r)
;; l and r either have equal lengths, or r has a trailing ...
(cond ((null? l) (null? r))
((vararg? (car l)) #t)
((null? r) #f)
((vararg? (car r)) (null? (cdr r)))
(else (sides-match? (cdr l) (cdr r)))))
(if (and (pair? x) (pair? lhss) (eq? (car x) 'tuple) (not (any assignment? (cdr x)))
(not (has-parameters? (cdr x)))
(sides-match? lhss (cdr x)))
;; (a, b, ...) = (x, y, ...)
(expand-forms
(tuple-to-assignments lhss x))
;; (a, b, ...) = other
(begin
;; like memq, but if last element of lhss 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? l x) #t)
(else (in-lhs? x (cdr lhss)))))))
;; in-lhs? also checks for invalid syntax, so always call it first
(let* ((xx (if (or (and (not (in-lhs? x lhss)) (symbol? x))
(ssavalue? x))
x (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)))
`(block
,@(if (> n 0) `((local ,st)) '())
,@ini
,@(map (lambda (i lhs)
(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)
(unnecessary ,xx))))))

;; move an assignment into the last statement of a block to keep more statements at top level
(define (sink-assignment lhs rhs)
(if (and (pair? rhs) (eq? (car rhs) 'block))
Expand Down Expand Up @@ -2102,67 +2162,24 @@
(call (top setproperty!) ,aa ,bb ,rr)
(unnecessary ,rr)))))
((tuple)
;; multiple assignment
(let ((lhss (cdr lhs))
(x (caddr e)))
(define (sides-match? l r)
;; l and r either have equal lengths, or r has a trailing ...
(cond ((null? l) (null? r))
((vararg? (car l)) #t)
((null? r) #f)
((vararg? (car r)) (null? (cdr r)))
(else (sides-match? (cdr l) (cdr r)))))
(if (and (pair? x) (pair? lhss) (eq? (car x) 'tuple) (not (any assignment? (cdr x)))
(not (has-parameters? (cdr x)))
(sides-match? lhss (cdr x)))
;; (a, b, ...) = (x, y, ...)
(expand-forms
(tuple-to-assignments lhss x))
;; (a, b, ...) = other
(begin
;; like memq, but if last element of lhss 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? l x) #t)
(else (in-lhs? x (cdr lhss)))))))
;; in-lhs? also checks for invalid syntax, so always call it first
(let* ((xx (if (or (and (not (in-lhs? x lhss)) (symbol? x))
(ssavalue? x))
x (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)))
`(block
,@(if (> n 0) `((local ,st)) '())
,@ini
,@(map (lambda (i lhs)
(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)
(unnecessary ,xx)))))))
(if (has-parameters? lhss)
;; property destructuring
(if (length= lhss 1)
(let* ((xx (if (symbol-like? x) x (make-ssavalue)))
(ini (if (eq? x xx) '() (list (sink-assignment xx (expand-forms x))))))
`(block
,@ini
,@(map (lambda (field)
(if (not (symbol? field))
(error (string "invalid assignment location \"" (deparse lhs) "\"")))
(expand-forms `(= ,field (call (top getproperty) ,xx (quote ,field)))))
(cdar lhss))
(unnecessary ,xx)))
(error (string "invalid assignment location \"" (deparse lhs) "\"")))
;; multiple assignment
(expand-tuple-destruct lhss x))))
((typed_hcat)
(error "invalid spacing in left side of indexed assignment"))
((typed_vcat)
Expand Down
29 changes: 29 additions & 0 deletions test/syntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2649,3 +2649,32 @@ end

# issue #38501
@test :"a $b $("str") c" == Expr(:string, "a ", :b, " ", Expr(:string, "str"), " c")

@testset "property destructuring" begin
res = begin (; num, den) = 1 // 2 end
@test res == 1 // 2
@test num == 1
@test den == 2

res = begin (; b, a) = (a=1, b=2, c=3) end
@test res == (a=1, b=2, c=3)
@test b == 2
@test a == 1

# could make this an error instead, but I think this is reasonable
res = begin (; a, b, a) = (a=5, b=6) end
@test res == (a=5, b=6)
@test a == 5
@test b == 6

@test_throws ErrorException (; a, b) = (x=1,)

@test Meta.isexpr(Meta.@lower(begin (a, b; c) = x end), :error)
@test Meta.isexpr(Meta.@lower(begin (a, b; c) = x, y end), :error)
@test Meta.isexpr(Meta.@lower(begin (; c, a.b) = x end), :error)

f((; a, b)) = a, b
@test f((b=3, a=4)) == (4, 3)
@test f((b=3, c=2, a=4)) == (4, 3)
@test_throws ErrorException f((;))
end

0 comments on commit d9e2632

Please sign in to comment.