From 6eb7f3d1a1e563ee3df0991d164258b7cef586fd Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Fri, 2 Feb 2018 08:04:48 -0500 Subject: [PATCH] RFC: required keyword arguments (#25830) * required keyword arguments --- NEWS.md | 3 +++ 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 | 24 +++++++++++++++++------- test/keywordargs.jl | 12 ++++++++++++ test/syntax.jl | 4 ---- 9 files changed, 58 insertions(+), 11 deletions(-) diff --git a/NEWS.md b/NEWS.md index 12ae3cdd26b01..c6c110a1a77f0 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 ([#25830]). + Language changes ---------------- diff --git a/base/boot.jl b/base/boot.jl index 83071073d87e9..5273c23ad20d6 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, + UndefKeywordError, # 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 UndefKeywordError <: Exception + var::Symbol +end struct MethodError <: Exception f diff --git a/base/docs/basedocs.jl b/base/docs/basedocs.jl index 7b2c3885a60b6..9cb15d2e43804 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 +""" + UndefKeywordError(var::Symbol) + +The required keyword argument `var` was not assigned in a function call. +""" +UndefKeywordError + """ OverflowError(msg) diff --git a/base/errorshow.jl b/base/errorshow.jl index 2cb16e9079ee3..baec065806b11 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::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] return showerror(io, ErrorException(""" diff --git a/doc/src/base/base.md b/doc/src/base/base.md index e14de8d6fe476..3a72511cd1683 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.UndefKeywordError Core.UndefRefError Core.UndefVarError Base.InitError diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index 9ee63d8716fe4..df9696fa4cde0 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 [`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 UndefKeywordError(: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 79d0d67affb18..754b0f470b41c 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -644,12 +644,22 @@ (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) + (define (throw-unassigned argname) + `(call (core throw) (call (core UndefKeywordError) (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 @@ -658,7 +668,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..20332327f6454 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 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 UndefKeywordError g(1) + @test_throws UndefKeywordError g(1, z=2) +end diff --git a/test/syntax.jl b/test/syntax.jl index d1abd33ccd8ca..fde541399498f 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)