diff --git a/NEWS.md b/NEWS.md index 6db22fa1f8be7..67f99d27536a4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -78,6 +78,8 @@ Build system changes New library functions --------------------- +* `@check cond [msg]` can be used to check if a condition holds and to error otherwise ([#41342]) + * `in!(x, s::AbstractSet)` will return whether `x` is in `s`, and insert `x` in `s` if not. * The new `Libc.mkfifo` function wraps the `mkfifo` C function on Unix platforms ([#34587]). diff --git a/base/error.jl b/base/error.jl index 37ceb39253e38..627e453b21bb2 100644 --- a/base/error.jl +++ b/base/error.jl @@ -237,6 +237,71 @@ macro assert(ex, msgs...) return :($(esc(ex)) ? $(nothing) : throw(AssertionError($msg))) end +# Copied from `macro assert` above for bootstrapping reasons +function prepare_error(ex, msgs...) + msg = isempty(msgs) ? ex : msgs[1] + if isa(msg, AbstractString) + msg = msg # pass-through + elseif !isempty(msgs) && (isa(msg, Expr) || isa(msg, Symbol)) + # message is an expression needing evaluating + msg = :(Main.Base.string($(esc(msg)))) + elseif isdefined(Main, :Base) && isdefined(Main.Base, :string) && applicable(Main.Base.string, msg) + msg = Main.Base.string(msg) + else + # string() might not be defined during bootstrap + msg = quote + msg = $(Expr(:quote,msg)) + isdefined(Main, :Base) ? Main.Base.string(msg) : + (Core.println(msg); "Error during bootstrap. See stdout.") + end + end + return msg +end + +""" + CheckError([msg]) + +The checked condition did not evaluate to `true`. +Optional argument `msg` is a descriptive error string. + +# Examples +```jldoctest +julia> @check false "this is not true" +ERROR: CheckError: this is not true +``` + +`CheckError` is usually thrown from [`@check`](@ref). +""" +struct CheckError <: Exception + msg::AbstractString +end +CheckError() = CheckError("") + +""" + @check cond [text] + +Throw an [`CheckError`](@ref) if `cond` is `false`. +Message `text` is optionally displayed upon check failure. + +Similar to [`@assert`](@ref), except `@check` is never disabled. + +# Examples +```jldoctest +julia> @check iseven(3) "3 is an odd number!" +ERROR: CheckError: 3 is an odd number! + +julia> @check isodd(3) "What even are numbers?" +``` + +!!! compat "Julia 1.8" + This macro was added in Julia 1.8. + +""" +macro check(ex, msgs...) + msg = prepare_error(ex, msgs...) + return :($(esc(ex)) ? $(nothing) : throw(CheckError($msg))) +end + struct ExponentialBackOff n::Int first_delay::Float64 diff --git a/base/errorshow.jl b/base/errorshow.jl index 74afc191fcc4d..5f9d092191d67 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -157,6 +157,7 @@ showerror(io::IO, ex::InterruptException) = print(io, "InterruptException:") showerror(io::IO, ex::ArgumentError) = print(io, "ArgumentError: ", ex.msg) showerror(io::IO, ex::DimensionMismatch) = print(io, "DimensionMismatch: ", ex.msg) showerror(io::IO, ex::AssertionError) = print(io, "AssertionError: ", ex.msg) +showerror(io::IO, ex::CheckError) = print(io, "CheckError: ", ex.msg) showerror(io::IO, ex::OverflowError) = print(io, "OverflowError: ", ex.msg) showerror(io::IO, ex::UndefKeywordError) = diff --git a/base/exports.jl b/base/exports.jl index 6b783eff9179b..286e8e3400646 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -128,6 +128,7 @@ export # Exceptions CanonicalIndexError, CapturedException, + CheckError, CompositeException, DimensionMismatch, EOFError, @@ -1063,6 +1064,7 @@ export @polly, @assert, + @check, @atomic, @atomicswap, @atomicreplace, diff --git a/doc/src/base/base.md b/doc/src/base/base.md index 64c635ed3043b..1ee4bf0a55575 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -395,10 +395,12 @@ Base.backtrace Base.catch_backtrace Base.current_exceptions Base.@assert +Base.@check Base.Experimental.register_error_hint Base.Experimental.show_error_hints Base.ArgumentError Base.AssertionError +Base.CheckError Core.BoundsError Base.CompositeException Base.DimensionMismatch diff --git a/test/misc.jl b/test/misc.jl index e0961c538921a..0dd8e8f17df17 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -5,61 +5,77 @@ include("testhelpers/withlocales.jl") # Tests that do not really go anywhere else -# test @assert macro -@test_throws AssertionError (@assert 1 == 2) -@test_throws AssertionError (@assert false) -@test_throws AssertionError (@assert false "this is a test") -@test_throws AssertionError (@assert false "this is a test" "another test") -@test_throws AssertionError (@assert false :a) -let - try - @assert 1 == 2 - error("unexpected") - catch ex - @test isa(ex, AssertionError) - @test occursin("1 == 2", ex.msg) - end -end -# test @assert message -let - try - @assert 1 == 2 "this is a test" - error("unexpected") - catch ex - @test isa(ex, AssertionError) - @test ex.msg == "this is a test" - end -end -# @assert only uses the first message string -let - try - @assert 1 == 2 "this is a test" "this is another test" - error("unexpected") - catch ex - @test isa(ex, AssertionError) - @test ex.msg == "this is a test" - end -end -# @assert calls string() on second argument -let - try - @assert 1 == 2 :random_object - error("unexpected") - catch ex - @test isa(ex, AssertionError) - @test !occursin("1 == 2", ex.msg) - @test occursin("random_object", ex.msg) +# test @assert and `@check` macro +# These have the same semantics (except `@assert` is allowed +# to be disabled in the future), so we test them with the same tests. +ASSERT_OR_CHECK = Ref(:assert) +macro assert_or_check(expr...) + if ASSERT_OR_CHECK[] == :assert + var"@assert"(__source__, __module__, expr...) + else + var"@check"(__source__, __module__, expr...) end end -# if the second argument is an expression, c -let deepthought(x, y) = 42 - try - @assert 1 == 2 string("the answer to the ultimate question: ", - deepthought(6, 9)) - error("unexpected") - catch ex - @test isa(ex, AssertionError) - @test ex.msg == "the answer to the ultimate question: 42" +@testset "@$(val)" for val in (:assert, :check) + ASSERT_OR_CHECK[] = val + @eval begin # eval to delay macro expansion until after we've assigned `val` + E = ASSERT_OR_CHECK[] == :assert ? AssertionError : CheckError + @test_throws E (@assert_or_check 1 == 2) + @test_throws E (@assert_or_check false) + @test_throws E (@assert_or_check false "this is a test") + @test_throws E (@assert_or_check false "this is a test" "another test") + @test_throws E (@assert_or_check false :a) + let + try + @assert_or_check 1 == 2 + error("unexpected") + catch ex + @test isa(ex, E) + @test occursin("1 == 2", ex.msg) + end + end + # test the macro's message + let + try + @assert_or_check 1 == 2 "this is a test" + error("unexpected") + catch ex + @test isa(ex, E) + @test ex.msg == "this is a test" + end + end + # the macro only uses the first message string + let + try + @assert_or_check 1 == 2 "this is a test" "this is another test" + error("unexpected") + catch ex + @test isa(ex, E) + @test ex.msg == "this is a test" + end + end + # the macro calls string() on second argument + let + try + @assert_or_check 1 == 2 :random_object + error("unexpected") + catch ex + @test isa(ex, E) + @test !occursin("1 == 2", ex.msg) + @test occursin("random_object", ex.msg) + end + end + # if the second argument is an expression, c + let deepthought(x, y) = 42 + try + @assert_or_check 1 == 2 string("the answer to the ultimate question: ", + deepthought(6, 9)) + error("unexpected") + catch ex + @test isa(ex, E) + @test ex.msg == "the answer to the ultimate question: 42" + end + end end end