From 5f60a841af69fd028fb223c7cae7369ec6f9d177 Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Sat, 16 Jan 2021 15:44:15 +0100 Subject: [PATCH 1/2] 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 --- src/julia-syntax.scm | 135 ++++++++++++++++++++++++------------------- test/syntax.jl | 29 ++++++++++ 2 files changed, 105 insertions(+), 59 deletions(-) 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 From d2e2a9b8588ae1c6072354f7ca06c0b872403a37 Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Thu, 21 Jan 2021 21:49:39 +0100 Subject: [PATCH 2/2] add NEWS entry --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) 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 ----------------