diff --git a/NEWS.md b/NEWS.md index edd474671f359..f9433e746f638 100644 --- a/NEWS.md +++ b/NEWS.md @@ -84,6 +84,11 @@ New library functions efficiently ([#35816]). * New function `addenv` for adding environment mappings into a `Cmd` object, returning the new `Cmd` object. * New function `insorted` for determining whether an element is in a sorted collection or not ([#37490]). +* New function `isdebug` for checking if julia is running in debug mode. + Packages use different precompile caches in debug and non-debug mode thus it + safe to use this function for compile time decisions. Also, note that + `@assert` now is a no-op unless `isdebug()` is true. Debug mode is activated + with the `-g2` flag. New library features -------------------- @@ -124,6 +129,8 @@ Standard library changes * The `Pkg.Artifacts` module has been imported as a separate standard library. It is still available as `Pkg.Artifacts`, however starting from Julia v1.6+, packages may import simply `Artifacts` without importing all of `Pkg` alongside. ([#37320]) +* The `@assert` macro is now only active when julia is started with a debug + level of 2 (`julia -g2`). #### LinearAlgebra diff --git a/base/Base.jl b/base/Base.jl index 207b571f30e16..a8f22a9922efc 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -66,6 +66,9 @@ time_ns() = ccall(:jl_hrtime, UInt64, ()) start_base_include = time_ns() +julia_debug = true +isdebug() = julia_debug::Bool + ## Load essential files and libraries include("essentials.jl") include("ctypes.jl") @@ -396,6 +399,7 @@ in_sysimage(pkgid::PkgId) = pkgid in _sysimage_modules if is_primary_base_module function __init__() + global julia_debug = Base.JLOptions().debug_level >= 2 # try to ensuremake sure OpenBLAS does not set CPU affinity (#1070, #9639) if !haskey(ENV, "OPENBLAS_MAIN_FREE") && !haskey(ENV, "GOTOBLAS_MAIN_FREE") ENV["OPENBLAS_MAIN_FREE"] = "1" diff --git a/base/error.jl b/base/error.jl index 16b66af68be12..49cd7869945ec 100644 --- a/base/error.jl +++ b/base/error.jl @@ -183,21 +183,25 @@ windowserror(p, b::Bool; extrainfo=nothing) = b ? windowserror(p, extrainfo=extr windowserror(p, code::UInt32=Libc.GetLastError(); extrainfo=nothing) = throw(Main.Base.SystemError(string(p), 0, WindowsErrorInfo(code, extrainfo))) -## assertion macro ## +## assertion and ifdebug macros ## + +""" + @ifdebug expr + +Equivalent to `@static if isdebug()` +""" +macro ifdebug(expr) + isdefined(@__MODULE__, :isdebug) && !(isdebug()) && return nothing + return :(esc(expr)) +end """ @assert cond [text] Throw an [`AssertionError`](@ref) if `cond` is `false`. Preferred syntax for writing assertions. -Message `text` is optionally displayed upon assertion failure. - -!!! warning - An assert might be disabled at various optimization levels. - Assert should therefore only be used as a debugging tool - and not used for authentication verification (e.g., verifying passwords), - nor should side effects needed for the function to work correctly - be used inside of asserts. +Message `text` is optionally displayed upon assertion failure. Only active when julia is started +in debug mode (`julia -g`). # Examples ```jldoctest @@ -208,6 +212,7 @@ julia> @assert isodd(3) "What even are numbers?" ``` """ macro assert(ex, msgs...) + isdefined(@__MODULE__, :isdebug) && !(isdebug()) && return nothing msg = isempty(msgs) ? ex : msgs[1] if isa(msg, AbstractString) msg = msg # pass-through diff --git a/base/exports.jl b/base/exports.jl index 2c0c628eec866..bcbfc47536ebf 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -780,6 +780,7 @@ export atreplinit, exit, ntuple, + isdebug, # I/O and events close, diff --git a/base/loading.jl b/base/loading.jl index 776f06202e0b4..04eb221e0179e 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -1231,6 +1231,7 @@ function compilecache_path(pkg::PkgId, cache::TOMLCache)::String crc = _crc32c(unsafe_string(JLOptions().image_file), crc) crc = _crc32c(unsafe_string(JLOptions().julia_bin), crc) crc = _crc32c(get_preferences_hash(pkg.uuid, cache), crc) + crc = _crc32c(isdebug(), crc) project_precompile_slug = slug(crc, 5) abspath(cachepath, string(entryfile, "_", project_precompile_slug, ".ji")) end @@ -1358,6 +1359,8 @@ function parse_cache_header(f::IO) end totbytes -= 4 + 4 + n2 + 8 end + julia_debug = Bool(read(f, UInt8)) + totbytes -= 1 prefs_hash = read(f, UInt64) totbytes -= 8 @assert totbytes == 12 "header of cache file appears to be corrupt" @@ -1372,7 +1375,7 @@ function parse_cache_header(f::IO) build_id = read(f, UInt64) # build id push!(required_modules, PkgId(uuid, sym) => build_id) end - return modules, (includes, requires), required_modules, srctextpos, prefs_hash + return modules, (includes, requires), required_modules, srctextpos, prefs_hash, julia_debug end function parse_cache_header(cachefile::String; srcfiles_only::Bool=false) @@ -1381,21 +1384,21 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false) !isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile.")) ret = parse_cache_header(io) srcfiles_only || return ret - modules, (includes, requires), required_modules, srctextpos, prefs_hash = ret + modules, (includes, requires), required_modules, srctextpos, prefs_hash, julia_debug = ret srcfiles = srctext_files(io, srctextpos) delidx = Int[] for (i, chi) in enumerate(includes) chi.filename ∈ srcfiles || push!(delidx, i) end deleteat!(includes, delidx) - return modules, (includes, requires), required_modules, srctextpos, prefs_hash + return modules, (includes, requires), required_modules, srctextpos, prefs_hash, julia_debug finally close(io) end end function cache_dependencies(f::IO) - defs, (includes, requires), modules, srctextpos, prefs_hash = parse_cache_header(f) + defs, (includes, requires), modules, srctextpos, prefs_hash, julia_debug = parse_cache_header(f) return modules, map(chi -> (chi.filename, chi.mtime), includes) # return just filename and mtime end @@ -1410,7 +1413,7 @@ function cache_dependencies(cachefile::String) end function read_dependency_src(io::IO, filename::AbstractString) - modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io) + modules, (includes, requires), required_modules, srctextpos, prefs_hash, julia_debug = parse_cache_header(io) srctextpos == 0 && error("no source-text stored in cache file") seek(io, srctextpos) return _read_dependency_src(io, filename) @@ -1496,7 +1499,7 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache) @debug "Rejecting cache file $cachefile due to it containing an invalid cache header" return true # invalid cache file end - modules, (includes, requires), required_modules, srctextpos, prefs_hash = parse_cache_header(io) + modules, (includes, requires), required_modules, srctextpos, prefs_hash, julia_debug = parse_cache_header(io) id = isempty(modules) ? nothing : first(modules).first modules = Dict{PkgId, UInt64}(modules) @@ -1572,6 +1575,11 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache) end if isa(id, PkgId) + curr_debug = isdebug() + if julia_debug != curr_debug + @debug "Rejecting cache file $cachefile because julia debug mode does not match" + return true + end curr_prefs_hash = get_preferences_hash(id.uuid, cache) if prefs_hash != curr_prefs_hash @debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))" diff --git a/doc/Makefile b/doc/Makefile index 3bf710c6a4a48..afccffe8331f8 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -7,7 +7,7 @@ SRCDIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) JULIAHOME := $(abspath $(SRCDIR)/..) SRCCACHE := $(abspath $(JULIAHOME)/deps/srccache) include $(JULIAHOME)/Make.inc -JULIA_EXECUTABLE := $(call spawn,$(build_bindir)/julia) --startup-file=no +JULIA_EXECUTABLE := $(call spawn,$(build_bindir)/julia) --startup-file=no -g2 .PHONY: help clean cleanall html pdf deps deploy diff --git a/src/dump.c b/src/dump.c index 287bf2d44701b..d6965aebbd45b 100644 --- a/src/dump.c +++ b/src/dump.c @@ -1124,6 +1124,14 @@ static int64_t write_dependency_list(ios_t *s, jl_array_t **udepsp, jl_array_t * } write_int32(s, 0); // terminator, for ease of reading + // Julia debug mode + jl_value_t *julia_debug = (jl_value_t*)jl_get_global(jl_base_module, jl_symbol("julia_debug")); + if (julia_debug) { + write_int8(s, jl_unbox_bool(julia_debug)); + } else { + write_int8(s, 0); + } + // Calculate Preferences hash for current package. jl_value_t *prefs_hash = NULL; if (jl_base_module) { diff --git a/test/assert.jl b/test/assert.jl new file mode 100644 index 0000000000000..312c17c8fda8e --- /dev/null +++ b/test/assert.jl @@ -0,0 +1,60 @@ +# This file is run from test/misc.jl separately since it needs to run with `-g2`. +using Test + +# 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) + 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" + end +end diff --git a/test/errorshow.jl b/test/errorshow.jl index 2d370d7f05246..f64902044a405 100644 --- a/test/errorshow.jl +++ b/test/errorshow.jl @@ -332,7 +332,7 @@ let undefvar @test err_str == "InterruptException:" err_str = @except_str throw(ArgumentError("not an error")) ArgumentError @test err_str == "ArgumentError: not an error" - err_str = @except_str @assert(false) AssertionError + err_str = read(`$(Base.julia_cmd()) -g2 -e 'try @assert false; catch e; showerror(stdout, e); end'`, String) @test err_str == "AssertionError: false" end diff --git a/test/exceptions.jl b/test/exceptions.jl index 7b8a54da2c6eb..67dbad17b7321 100644 --- a/test/exceptions.jl +++ b/test/exceptions.jl @@ -369,6 +369,8 @@ end end # issue #36527 +s = raw""" +using Test function f36527() caught = false 🏡 = Core.eval(Main, :(module asdf36527 end)) @@ -383,6 +385,8 @@ function f36527() end @test f36527() +""" +@test success(`$(Base.julia_cmd()) -g2 -e $s`) # accessing an undefined var in tail position in a catch block function undef_var_in_catch() diff --git a/test/misc.jl b/test/misc.jl index 920a03abdb520..425dc14a79487 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -3,64 +3,8 @@ isdefined(Main, :FakePTYs) || @eval Main include("testhelpers/FakePTYs.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) - 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" - end -end +assert_file = joinpath(@__DIR__, "assert.jl") +@test success(`$(Base.julia_cmd()) -g2 $assert_file`) let # test the process title functions, issue #9957 oldtitle = Sys.get_process_title() @@ -770,9 +714,9 @@ end @kwdef struct TestInnerConstructor a = 1 - TestInnerConstructor(a::Int) = (@assert a>0; new(a)) + TestInnerConstructor(a::Int) = (a>0 || error(); new(a)) function TestInnerConstructor(a::String) - @assert length(a) > 0 + length(a) > 0 || error() new(a) end end @@ -780,9 +724,9 @@ end @testset "@kwdef inner constructor" begin @test TestInnerConstructor() == TestInnerConstructor(1) @test TestInnerConstructor(a=2) == TestInnerConstructor(2) - @test_throws AssertionError TestInnerConstructor(a=0) + @test_throws ErrorException TestInnerConstructor(a=0) @test TestInnerConstructor(a="2") == TestInnerConstructor("2") - @test_throws AssertionError TestInnerConstructor(a="") + @test_throws ErrorException TestInnerConstructor(a="") end const outsidevar = 7 @@ -819,7 +763,7 @@ end @testset "Pointer to unsigned/signed integer" begin # assuming UInt and Ptr have the same size - @assert sizeof(UInt) == sizeof(Ptr{Nothing}) + @test sizeof(UInt) == sizeof(Ptr{Nothing}) uint = UInt(0x12345678) sint = signed(uint) ptr = reinterpret(Ptr{Nothing}, uint) diff --git a/test/precompile.jl b/test/precompile.jl index 92ff419bf5997..4373e4b811fb9 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -901,4 +901,73 @@ let end end +# Debug mode +let + tmp = mktempdir() + try + cd(tmp) do + pkg_content(mod) = """ + """ + + write("Project.toml", """ + [deps] + A = "065497a9-3da1-45c2-901d-78aba5544f04" + B = "d52a13bc-ce4c-4a58-a416-e1c8b2d677de" + """) + + write("Manifest.toml", """ + [[A]] + deps = ["B"] + path = "A" + uuid = "065497a9-3da1-45c2-901d-78aba5544f04" + + [[B]] + path = "B" + uuid = "d52a13bc-ce4c-4a58-a416-e1c8b2d677de" + """) + + mkpath("A/src/") + write("A/src/A.jl", """ + module A + using B + const is_debug = isdebug() + maybe_assert() = @assert false + end + """) + + mkpath("B/src/") + write("B/src/B.jl", """ + module B + const is_debug = isdebug() + maybe_assert() = @assert false + end + """) + + s = """ + using Test + using A + @test A.is_debug == true + @test A.B.is_debug == true + @test_throws AssertionError A.maybe_assert() + @test_throws AssertionError A.B.maybe_assert() + """ + @test success(Cmd(`$(Base.julia_cmd()) --project -g2 -e $s`; env = ["JULIA_DEPOT_PATH"=>tmp])) + + s = """ + using Test + using A + @test A.is_debug == false + @test A.B.is_debug == false + @test A.maybe_assert() === nothing + @test A.B.maybe_assert() === nothing + """ + @test success(Cmd(`$(Base.julia_cmd()) --project -e $s`; env = ["JULIA_DEPOT_PATH"=>tmp])) + end + finally + rm(tmp; recursive=true) + end +end + + + end # !withenv