From d63ae558bfcfb1fe3fac79c973edbe2f926ba238 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Wed, 31 Jan 2018 14:38:18 -0500 Subject: [PATCH 1/5] required keyword arguments --- NEWS.md | 5 ++++- base/boot.jl | 4 ++++ base/docs/basedocs.jl | 7 +++++++ base/errorshow.jl | 3 +++ doc/src/base/base.md | 1 + doc/src/manual/functions.md | 11 +++++++++++ src/julia-syntax.scm | 16 +++++++++++++++- test/keywordargs.jl | 12 ++++++++++++ 8 files changed, 57 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 5d34d215ba8d0..f7d392fd737f4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -44,6 +44,9 @@ New language features * Values for `Enum`s can now be specified inside of a `begin` block when using the `@enum` macro ([#25424]). + * Keyword arguments can be required: if a default value is omitted, then an + exception is thrown if the caller does not assign the keyword a value ([#5111]). + Language changes ---------------- @@ -1260,4 +1263,4 @@ Command-line option changes [#25622]: https://github.com/JuliaLang/julia/issues/25622 [#25634]: https://github.com/JuliaLang/julia/issues/25634 [#25654]: https://github.com/JuliaLang/julia/issues/25654 -[#25655]: https://github.com/JuliaLang/julia/issues/25655 \ No newline at end of file +[#25655]: https://github.com/JuliaLang/julia/issues/25655 diff --git a/base/boot.jl b/base/boot.jl index 83071073d87e9..6b9f67a3e4a2f 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -139,6 +139,7 @@ export InterruptException, InexactError, OutOfMemoryError, ReadOnlyMemoryError, OverflowError, StackOverflowError, SegmentationFault, UndefRefError, UndefVarError, TypeError, ArgumentError, MethodError, AssertionError, LoadError, InitError, + UnassignedKeyword, # AST representation Expr, GotoNode, LabelNode, LineNumberNode, QuoteNode, GlobalRef, NewvarNode, SSAValue, Slot, SlotNumber, TypedSlot, @@ -253,6 +254,9 @@ end struct ArgumentError <: Exception msg::AbstractString end +struct UnassignedKeyword <: Exception + var::Symbol +end struct MethodError <: Exception f diff --git a/base/docs/basedocs.jl b/base/docs/basedocs.jl index 7b2c3885a60b6..b1c72afc18c3e 100644 --- a/base/docs/basedocs.jl +++ b/base/docs/basedocs.jl @@ -1019,6 +1019,13 @@ A symbol in the current scope is not defined. """ UndefVarError +""" + UnassignedKeyword(var::Symbol) + +The required keyword argument `var` was not assigned in a function call. +""" +UnassignedKeyword + """ OverflowError(msg) diff --git a/base/errorshow.jl b/base/errorshow.jl index 2cb16e9079ee3..f55bead9396b8 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -134,6 +134,9 @@ showerror(io::IO, ex::ArgumentError) = print(io, "ArgumentError: $(ex.msg)") showerror(io::IO, ex::AssertionError) = print(io, "AssertionError: $(ex.msg)") showerror(io::IO, ex::OverflowError) = print(io, "OverflowError: $(ex.msg)") +showerror(io::IO, ex::UnassignedKeyword) = + print(io, "UnassignedKeyword: keyword argument $(ex.var) not assigned") + function showerror(io::IO, ex::UndefVarError) if ex.var in [:UTF16String, :UTF32String, :WString, :utf16, :utf32, :wstring, :RepString] return showerror(io, ErrorException(""" diff --git a/doc/src/base/base.md b/doc/src/base/base.md index e14de8d6fe476..f2ab999f98f96 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -298,6 +298,7 @@ Base.ParseError Core.StackOverflowError Base.SystemError Core.TypeError +Core.UnassignedKeyword Core.UndefRefError Core.UndefVarError Base.InitError diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index 9ee63d8716fe4..b9c56d3bd38f8 100644 --- a/doc/src/manual/functions.md +++ b/doc/src/manual/functions.md @@ -518,6 +518,17 @@ function f(x; y=0, kwargs...) end ``` +If a keyword argument is not assigned a default value in the method definition, +then it is *required*: an [`UnassignedKeyword`](@ref) exception will be thrown +if the caller does not assign it a value: +```julia +function f(x; y) + ### +end +f(3, y=5) # ok, y is assigned +f(3) # throws UnassignedKeyword(:y) +``` + Inside `f`, `kwargs` will be a named tuple. Named tuples (as well as dictionaries) can be passed as keyword arguments using a semicolon in a call, e.g. `f(x, z=1; kwargs...)`. diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 25740675aa348..2f7643311a998 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -651,6 +651,20 @@ (deparse (car invalid)) "\" (expected assignment)")))))))) +; replace unassigned kw args with assignment to throw() call (forcing the caller to assign the keyword) +(define (throw-unassigned-kw-args argl) + (define (throw-unassigned argname) + `(call (core throw) (call (core UnassignedKeyword) (inert ,argname)))) + (if (has-parameters? argl) + (cons (cons 'parameters + (map (lambda (x) + (cond ((symbol? x) `(kw ,x ,(throw-unassigned x))) + ((decl? x) `(kw ,x ,(throw-unassigned (cadr x)))) + (else x))) + (cdar argl))) + (cdr argl)) + argl)) + ;; method-def-expr checks for keyword arguments, and if there are any, calls ;; keywords-method-def-expr to expand the definition into several method ;; definitions that do not use keyword arguments. @@ -658,7 +672,7 @@ ;; which handles optional positional arguments by adding the needed small ;; boilerplate definitions. (define (method-def-expr name sparams argl body rett) - (let ((argl (remove-empty-parameters argl))) + (let ((argl (throw-unassigned-kw-args (remove-empty-parameters argl)))) (if (has-parameters? argl) ;; has keywords (begin (check-kw-args (cdar argl)) diff --git a/test/keywordargs.jl b/test/keywordargs.jl index 47ffc65e4d0dd..2cf5463b4d104 100644 --- a/test/keywordargs.jl +++ b/test/keywordargs.jl @@ -308,3 +308,15 @@ end ((1, 3, 5, 6, 7), (a = 2, b = 4, c = 8, d = 9, f = 10)) end + +@testset "required keyword arguments" begin + f(x; y, z=3) = x + 2y + 3z + @test f(1, y=2) === 14 === f(10, y=2, z=0) + @test_throws UnassignedKeyword f(1) + @test_throws UnassignedKeyword f(1, z=2) + g(x; y::Int, z=3) = x + 2y + 3z + @test g(1, y=2) === 14 === g(10, y=2, z=0) + @test_throws TypeError g(1, y=2.3) + @test_throws UnassignedKeyword g(1) + @test_throws UnassignedKeyword g(1, z=2) +end From e742fbe5a656f8be9cea8eef03d45a7a0726724f Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Wed, 31 Jan 2018 14:49:33 -0500 Subject: [PATCH 2/5] simplified kw check since assignments no longer required --- src/julia-syntax.scm | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 2f7643311a998..b0d0d0191c094 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -644,12 +644,8 @@ (if (pair? invalid) (if (and (pair? (car invalid)) (eq? 'parameters (caar invalid))) (error "more than one semicolon in argument list") - (cond ((symbol? (car invalid)) - (error (string "keyword argument \"" (car invalid) "\" needs a default value"))) - (else - (error (string "invalid keyword argument syntax \"" - (deparse (car invalid)) - "\" (expected assignment)")))))))) + (error (string "invalid keyword argument syntax \"" + (deparse (car invalid)) "\"")))))) ; replace unassigned kw args with assignment to throw() call (forcing the caller to assign the keyword) (define (throw-unassigned-kw-args argl) From 2bd7886cc661ad8c387f0c041f3c53aff2f97a52 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Wed, 31 Jan 2018 14:49:50 -0500 Subject: [PATCH 3/5] fix NEWS pr --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index f7d392fd737f4..7ff0c1bd287e2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -45,7 +45,7 @@ New language features `@enum` macro ([#25424]). * Keyword arguments can be required: if a default value is omitted, then an - exception is thrown if the caller does not assign the keyword a value ([#5111]). + exception is thrown if the caller does not assign the keyword a value ([#25830]). Language changes ---------------- From adad2ab37a6c1f896ff67936fb37bbdde000e0fe Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Wed, 31 Jan 2018 16:37:38 -0500 Subject: [PATCH 4/5] rename UnassignedKeyword -> UndefKeywordError for consistency with e.g. UndefVarError --- base/boot.jl | 4 ++-- base/docs/basedocs.jl | 4 ++-- base/errorshow.jl | 4 ++-- doc/src/base/base.md | 2 +- doc/src/manual/functions.md | 4 ++-- src/julia-syntax.scm | 2 +- test/keywordargs.jl | 8 ++++---- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/base/boot.jl b/base/boot.jl index 6b9f67a3e4a2f..5273c23ad20d6 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -139,7 +139,7 @@ export InterruptException, InexactError, OutOfMemoryError, ReadOnlyMemoryError, OverflowError, StackOverflowError, SegmentationFault, UndefRefError, UndefVarError, TypeError, ArgumentError, MethodError, AssertionError, LoadError, InitError, - UnassignedKeyword, + UndefKeywordError, # AST representation Expr, GotoNode, LabelNode, LineNumberNode, QuoteNode, GlobalRef, NewvarNode, SSAValue, Slot, SlotNumber, TypedSlot, @@ -254,7 +254,7 @@ end struct ArgumentError <: Exception msg::AbstractString end -struct UnassignedKeyword <: Exception +struct UndefKeywordError <: Exception var::Symbol end diff --git a/base/docs/basedocs.jl b/base/docs/basedocs.jl index b1c72afc18c3e..9cb15d2e43804 100644 --- a/base/docs/basedocs.jl +++ b/base/docs/basedocs.jl @@ -1020,11 +1020,11 @@ A symbol in the current scope is not defined. UndefVarError """ - UnassignedKeyword(var::Symbol) + UndefKeywordError(var::Symbol) The required keyword argument `var` was not assigned in a function call. """ -UnassignedKeyword +UndefKeywordError """ OverflowError(msg) diff --git a/base/errorshow.jl b/base/errorshow.jl index f55bead9396b8..baec065806b11 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -134,8 +134,8 @@ showerror(io::IO, ex::ArgumentError) = print(io, "ArgumentError: $(ex.msg)") showerror(io::IO, ex::AssertionError) = print(io, "AssertionError: $(ex.msg)") showerror(io::IO, ex::OverflowError) = print(io, "OverflowError: $(ex.msg)") -showerror(io::IO, ex::UnassignedKeyword) = - print(io, "UnassignedKeyword: keyword argument $(ex.var) not assigned") +showerror(io::IO, ex::UndefKeywordError) = + print(io, "UndefKeywordError: keyword argument $(ex.var) not assigned") function showerror(io::IO, ex::UndefVarError) if ex.var in [:UTF16String, :UTF32String, :WString, :utf16, :utf32, :wstring, :RepString] diff --git a/doc/src/base/base.md b/doc/src/base/base.md index f2ab999f98f96..3a72511cd1683 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -298,7 +298,7 @@ Base.ParseError Core.StackOverflowError Base.SystemError Core.TypeError -Core.UnassignedKeyword +Core.UndefKeywordError Core.UndefRefError Core.UndefVarError Base.InitError diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index b9c56d3bd38f8..df9696fa4cde0 100644 --- a/doc/src/manual/functions.md +++ b/doc/src/manual/functions.md @@ -519,14 +519,14 @@ end ``` If a keyword argument is not assigned a default value in the method definition, -then it is *required*: an [`UnassignedKeyword`](@ref) exception will be thrown +then it is *required*: an [`UndefKeywordError`](@ref) exception will be thrown if the caller does not assign it a value: ```julia function f(x; y) ### end f(3, y=5) # ok, y is assigned -f(3) # throws UnassignedKeyword(:y) +f(3) # throws UndefKeywordError(:y) ``` Inside `f`, `kwargs` will be a named tuple. Named tuples (as well as dictionaries) can be passed as diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index b0d0d0191c094..16885067017e6 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -650,7 +650,7 @@ ; replace unassigned kw args with assignment to throw() call (forcing the caller to assign the keyword) (define (throw-unassigned-kw-args argl) (define (throw-unassigned argname) - `(call (core throw) (call (core UnassignedKeyword) (inert ,argname)))) + `(call (core throw) (call (core UndefKeywordError) (inert ,argname)))) (if (has-parameters? argl) (cons (cons 'parameters (map (lambda (x) diff --git a/test/keywordargs.jl b/test/keywordargs.jl index 2cf5463b4d104..20332327f6454 100644 --- a/test/keywordargs.jl +++ b/test/keywordargs.jl @@ -312,11 +312,11 @@ end @testset "required keyword arguments" begin f(x; y, z=3) = x + 2y + 3z @test f(1, y=2) === 14 === f(10, y=2, z=0) - @test_throws UnassignedKeyword f(1) - @test_throws UnassignedKeyword f(1, z=2) + @test_throws UndefKeywordError f(1) + @test_throws UndefKeywordError f(1, z=2) g(x; y::Int, z=3) = x + 2y + 3z @test g(1, y=2) === 14 === g(10, y=2, z=0) @test_throws TypeError g(1, y=2.3) - @test_throws UnassignedKeyword g(1) - @test_throws UnassignedKeyword g(1, z=2) + @test_throws UndefKeywordError g(1) + @test_throws UndefKeywordError g(1, z=2) end From a7d1aab911c85366e035e3460f9ac4280e7a9be8 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Thu, 1 Feb 2018 08:04:36 -0500 Subject: [PATCH 5/5] rm test that is no longer applicable --- test/syntax.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/syntax.jl b/test/syntax.jl index 39722cbff0345..da2c108d28e23 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -500,10 +500,6 @@ let m_error, error_out, filename = Base.source_path() error_out = sprint(showerror, m_error) @test startswith(error_out, "ArgumentError: invalid type for argument number 1 in method definition for method_c6 at $filename:") - m_error = try @eval method_c6(A; B) = 3; catch e; e; end - error_out = sprint(showerror, m_error) - @test error_out == "syntax: keyword argument \"B\" needs a default value" - # issue #20614 m_error = try @eval foo(types::NTuple{N}, values::Vararg{Any,N}, c) where {N} = nothing; catch e; e; end error_out = sprint(showerror, m_error)