diff --git a/NEWS.md b/NEWS.md index d994c3d2f17fc..de8a441629f6d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 ---------------- diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 0cf133f09fcfb..ac798109ed1d5 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -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)) @@ -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) diff --git a/test/syntax.jl b/test/syntax.jl index 085794b72d859..ac4ccef86660b 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -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