Defensive programming & assert


#1

I come from the C++ world and I would like to use Julia more often. I generally use a lot of asserts in my code (a “defensive” programming approach), for instance imagine a function reading the first N records of a file. In pseudo C++ code you have something like:

 // read N first records
 //
 void read_file_N(std::istream& file_in,const int N) 
 {
      assert(N>=0);       // logical error: coder/caller responsibility is involved

      for(i=0;i<N;i++) {
   
           file >> data;

           // coder/caller responsibility is _not_ involved (I/O error)
           //
           if(!data.well_formed())  { 
                throw error("Error in file, data is not well formed"); 
           }
      ...
   }
}

There are (at least) two kind of errors. Logical ones, involving coder responsibility and errors (like corrupted I/O etc.) where coder is not responsible. For the first ones I use a lot of asserts, for the second one I use exceptions. In C/C++ I can easily remove the assert thanks to a compiler flag (-DNDEBUG) thus there is no run-time penalty in production code. However, when I code in Julia (I am quite new with this language) I have the feeling that these two notions are mixed and that we can not take the same approach: AFAIK Julia @assert/assert is close to my C++ exception usage, but there is no Julia equivalent to C++ assert. This is a real problem for me and the way I code.

Do I miss something?
What can I do to have a C/C++ equivalent to assert in Julia (with the constraint that we can remove them in production code)?


#2

you can use something like:

module MyModule
const DNDEBUG =  true
macro c_assert(boolean)
    if DNDEBUG
        :($boolean || error("Assertion $($a) == $($b) failed"))
    end
end
end

julia> using MyModule
julia> @c_assert 1 == 2
ERROR: Assertion 1 == 2 failed

With DNDEBUG = false

julia> test() = @c_assert 1 == 2
test (generic function with 1 method)
julia> test()
julia> @code_llvm test()
define void @julia_test_69494() #0 !dbg !5 {
top:
  ret void
}

Note, that this will recompile the optimized version as well, when you change the flag in the source.


#3

Thanks for the feedback (and sorry for my first post bad formatting).
I was thinking to something similar but I thought that maybe there was a special/intergrated Julia way for it.

I think that there is a typo, the macro should be something like

:($boolean || error(“Assertion $boolean failed”))

but this only print “false”, how to print it in its unevaluted form?


#4

Ah yeah, I first had another version.
Try:

julia> macro c_assert(boolean)
           if DNDEBUG
               :($(esc(boolean)) || error("Assertion $($(QuoteNode(boolean))) failed"))
           end
       end

#5

Works great, thanks!


#6

A little less magical:

macro c_assert(boolean)
      if DNDEBUG
           message = string("Assertion: ", boolean, " failed")
           :($(esc(boolean)) || error($message))
      end
end

#7

Reference: omit assert under --optimize; introduce@check macro?


#8

Yes, I have seen that however on my Julia V0.6, @check macro is not found.


#9

Indeed it hasn’t been implemented yet, but it is a popular wanted feature, I’d love built-in support too, just wanted people reading this know.


#10

This is my take on this thread / issue: https://github.com/JuliaLang/julia/pull/25576


#11

Here’s a way this can be done for modules without breaking precompilation.

module DebugAsserts

export debug_asserts, @dassert

function debug_asserts(m::Module, dodebug::Bool)
    m.eval(:(_debug_enabled() = $dodebug))
    nothing
end

# Like @assert, but only active in debug mode
macro dassert(exs...)
    if !isdefined(__module__, :_debug_enabled)
        eval(__module__, :(_debug_enabled() = false))
    end
    quote
        if $__module__._debug_enabled()
            @assert $(map(esc,exs)...)
        end
    end
end

end

#12

AFAIU, The way this works is exploiting that #265 (reliable redefinition of methods) is fixed, this automatically recompiles all dependent functions that use @dassert every time debug_asserts is called. Depending on how many functions uses @dassert (or how many functions are in the call graph of any function that uses it) this will have to recompile a different number of functions but, in the worst case, using debug_asserts will cause a full recompilation of the whole package.


#13

That’s right, this relies on the compiler to recompile all methods which use @dassert when the associated _debug_enabled is redefined via #265. You can use this @dassert in your own package exactly the same way as @assert, but the expression should be completely optimized away unless you call debug_asserts(MyModule, true).


#14

Ah, clever. And since we don’t print the method replacement warning anymore, seems like this should be fairly seamless even too. I’m not sure we would want to put a trick like that in Base (to avoid potential confusion over when world-age changes become visible), but on-the-other-hand, it’s such a simple mechanism (and world-age visibility constraints usually aren’t too noticeable), so perhaps, why not!