Skip to content

Commit 8ebedd6

Browse files
authored
Merge pull request #22958 from JuliaLang/jb/fix17240
fix #17240, evaluate keyword argument defaults in successive scopes
2 parents 4b296b7 + 0af919b commit 8ebedd6

File tree

4 files changed

+60
-60
lines changed

4 files changed

+60
-60
lines changed

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Language changes
1919
* Declaring arguments as `x::ANY` to avoid specialization has been replaced
2020
by `@nospecialize x`. ([#22666]).
2121

22+
* Keyword argument default values are now evaluated in successive scopes ---
23+
the scope for each expression includes only previous keyword arguments, in
24+
left-to-right order ([#17240]).
25+
2226
* The parsing of `1<<2*3` as `1<<(2*3)` is deprecated, and will change to
2327
`(1<<2)*3` in a future version ([#13079]).
2428

doc/src/manual/functions.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -472,9 +472,8 @@ this example, `width` is certain to have the value `2`.
472472

473473
## Evaluation Scope of Default Values
474474

475-
Optional and keyword arguments differ slightly in how their default values are evaluated. When
476-
optional argument default expressions are evaluated, only *previous* arguments are in scope. In
477-
contrast, *all* the arguments are in scope when keyword arguments default expressions are evaluated.
475+
When optional and keyword argument default expressions are evaluated, only *previous* arguments are in
476+
scope.
478477
For example, given this definition:
479478

480479
```julia
@@ -483,11 +482,7 @@ function f(x, a=b, b=1)
483482
end
484483
```
485484

486-
the `b` in `a=b` refers to a `b` in an outer scope, not the subsequent argument `b`. However,
487-
if `a` and `b` were keyword arguments instead, then both would be created in the same scope and
488-
the `b` in `a=b` would refer to the subsequent argument `b` (shadowing any `b` in an outer scope),
489-
which would result in an undefined variable error (since the default expressions are evaluated
490-
left-to-right, and `b` has not been assigned yet).
485+
the `b` in `a=b` refers to a `b` in an outer scope, not the subsequent argument `b`.
491486

492487
## Do-Block Syntax for Function Arguments
493488

src/julia-syntax.scm

Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -367,12 +367,13 @@
367367
`(block (method ,name) ,mdef (unnecessary ,name)) ;; return the function
368368
mdef)))))
369369

370-
;; keyword default values that can be assigned right away. however, this creates
371-
;; a quasi-bug (part of issue #9535) where it can be hard to predict when a
372-
;; keyword argument will throw an UndefVarError.
373-
(define (const-default? x)
374-
(or (number? x) (string? x) (char? x) (and (pair? x) (memq (car x) '(quote inert)))
375-
(eq? x 'true) (eq? x 'false)))
370+
;; wrap expr in nested scopes assigning names to vals
371+
(define (scopenest names vals expr)
372+
(if (null? names)
373+
expr
374+
`(let (block
375+
,(scopenest (cdr names) (cdr vals) expr))
376+
(= ,(car names) ,(car vals)))))
376377

377378
(define empty-vector-any '(call (core AnyVector) 0))
378379

@@ -434,24 +435,23 @@
434435
(rkw (if (null? restkw) '() (symbol (string (car restkw) "..."))))
435436
(mangled (symbol (string "#" (if name (undot-name name) 'call) "#"
436437
(string (current-julia-module-counter)))))
437-
(flags (map (lambda (x) (gensy)) vals)))
438+
(tempnames (map (lambda (x) (gensy)) keynames)))
438439
`(block
439440
;; call with no keyword args
440441
,(method-def-expr-
441442
name positional-sparams (append pargl vararg)
442443
`(block
443444
,@prologue
444-
,@(if (not ordered-defaults)
445-
'()
446-
(append! (map (lambda (kwname) `(local ,kwname)) keynames)
447-
(map make-assignment keynames vals)))
448-
;; call mangled(vals..., [rest_kw,] pargs..., [vararg]...)
449-
(return (call ,mangled
450-
,@(if ordered-defaults keynames vals)
451-
,@(if (null? restkw) '() (list empty-vector-any))
452-
,@(map arg-name pargl)
453-
,@(if (null? vararg) '()
454-
(list `(... ,(arg-name (car vararg))))))))
445+
,(let (;; call mangled(vals..., [rest_kw,] pargs..., [vararg]...)
446+
(ret `(return (call ,mangled
447+
,@(if ordered-defaults keynames vals)
448+
,@(if (null? restkw) '() (list empty-vector-any))
449+
,@(map arg-name pargl)
450+
,@(if (null? vararg) '()
451+
(list `(... ,(arg-name (car vararg)))))))))
452+
(if ordered-defaults
453+
(scopenest keynames vals ret)
454+
ret)))
455455
#f)
456456

457457
;; call with keyword args pre-sorted - original method code goes here
@@ -483,14 +483,9 @@
483483
,(if (any kwarg? pargl) (gensy) UNUSED)
484484
(call (core kwftype) ,ftype)) (:: ,kw (core AnyVector)) ,@pargl ,@vararg)
485485
`(block
486-
;; initialize keyword args to their defaults, or set a flag telling
487-
;; whether this keyword needs to be set.
488-
,@(map (lambda (kwname) `(local ,kwname)) keynames)
489-
,@(map (lambda (name dflt flag)
490-
(if (const-default? dflt)
491-
`(= ,name ,dflt)
492-
`(= ,flag true)))
493-
keynames vals flags)
486+
;; temp variables that will be assigned if their corresponding keywords are passed.
487+
;; `isdefined` is then used to check whether default values should be evaluated.
488+
,@(map (lambda (v) `(local ,v)) tempnames)
494489
,@(if (null? restkw) '()
495490
`((= ,rkw ,empty-vector-any)))
496491
;; for i = 1:(length(kw)>>1)
@@ -499,8 +494,8 @@
499494
;; ii = i*2 - 1
500495
(= ,ii (call (top -) (call (top *) ,i 2) 1))
501496
(= ,elt (call (core arrayref) ,kw ,ii))
502-
,(foldl (lambda (kvf else)
503-
(let* ((k (car kvf))
497+
,(foldl (lambda (kn else)
498+
(let* ((k (car kn))
504499
(rval0 `(call (core arrayref) ,kw
505500
(call (top +) ,ii 1)))
506501
;; note: if the "declared" type of a KW arg
@@ -528,13 +523,9 @@
528523
,T)
529524
T)))
530525
rval0)))
531-
;; if kw[ii] == 'k; k = kw[ii+1]::Type; end
532-
`(if (comparison ,elt === (quote ,(decl-var k)))
533-
(block
534-
(= ,(decl-var k) ,rval)
535-
,@(if (not (const-default? (cadr kvf)))
536-
`((= ,(caddr kvf) false))
537-
'()))
526+
;; if kw[ii] == 'k; k_temp = kw[ii+1]::Type; end
527+
`(if (comparison ,elt === (quote ,(cdr kn)))
528+
(= ,(decl-var k) ,rval)
538529
,else)))
539530
(if (null? restkw)
540531
;; if no rest kw, give error for unrecognized
@@ -547,21 +538,22 @@
547538
,rkw (tuple ,elt
548539
(call (core arrayref) ,kw
549540
(call (top +) ,ii 1)))))
550-
(map list vars vals flags))))
541+
(map (lambda (k temp)
542+
(cons (if (decl? k) `(,(car k) ,temp ,(caddr k)) temp)
543+
(decl-var k)))
544+
vars tempnames))))
551545
;; set keywords that weren't present to their default values
552-
,@(apply append
553-
(map (lambda (name dflt flag)
554-
(if (const-default? dflt)
555-
'()
556-
`((if ,flag (= ,name ,dflt)))))
557-
keynames vals flags))
558-
;; finally, call the core function
559-
(return (call ,mangled
560-
,@keynames
561-
,@(if (null? restkw) '() (list rkw))
562-
,@(map arg-name pargl)
563-
,@(if (null? vararg) '()
564-
(list `(... ,(arg-name (car vararg))))))))
546+
,(scopenest keynames
547+
(map (lambda (v dflt) `(if (isdefined ,v)
548+
,v
549+
,dflt))
550+
tempnames vals)
551+
`(return (call ,mangled ;; finally, call the core function
552+
,@keynames
553+
,@(if (null? restkw) '() (list rkw))
554+
,@(map arg-name pargl)
555+
,@(if (null? vararg) '()
556+
(list `(... ,(arg-name (car vararg)))))))))
565557
#f)
566558
;; return primary function
567559
,(if (not (symbol? name))
@@ -2590,7 +2582,7 @@
25902582
glob)
25912583
(if lam ;; update in-place the list of local variables in lam
25922584
(set-car! (cddr lam)
2593-
(append! (caddr lam) real-new-vars real-new-vars-def)))
2585+
(append real-new-vars real-new-vars-def (caddr lam))))
25942586
(insert-after-meta ;; return the new, expanded scope-block
25952587
(if (and (pair? body) (eq? (car body) 'block))
25962588
body

test/keywordargs.jl

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,10 @@ f9948, getx9948 = let
219219
getx() = x
220220
h, getx
221221
end
222-
@test_throws UndefVarError f9948()
222+
@test f9948() == 3
223223
@test getx9948() == 3
224224
@test f9948(x=5) == 5
225-
@test_throws UndefVarError f9948()
225+
@test f9948() == 3
226226
@test getx9948() == 3
227227

228228
# issue #17785 - handle all sources of kwargs left-to-right
@@ -285,3 +285,12 @@ let a = 0
285285
g21518()(; :kw=>1)
286286
@test a == 2
287287
end
288+
289+
# issue #17240 - evaluate default expressions in nested scopes
290+
let a = 10
291+
f17240(;a=a-1, b=2a) = (a, b)
292+
@test f17240() == (9, 18)
293+
@test f17240(a=2) == (2, 4)
294+
@test f17240(b=3) == (9, 3)
295+
@test f17240(a=2, b=1) == (2, 1)
296+
end

0 commit comments

Comments
 (0)