crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.22k stars 1.61k forks source link

assert macro for non-release #4263

Open will opened 7 years ago

will commented 7 years ago

I've been working on some numeric code that benefits from a lot of invariant checks while developing and testing. However having them all run in release mode is not necessary.

I made a macro that has been helpful. Is this something that you all would want in the language itself?

~ ➤ cat assert.cr
macro assert(exp, file = __FILE__, line = __LINE__)
  {% if !flag?(:release) %}
    unless {{exp}}
      raise "Assertion Failed #{{{file}}}:#{{{line}}}"
    end
  {% end %}
end

def test
  puts "evaulated"
  false
end

assert test
~ ➤ crystal run assert.cr
evaulated
Assertion Failed /Users/will/assert.cr:14 (Exception)
0x10750b685: *CallStack::unwind:Array(Pointer(Void)) at ??
0x10750b621: *CallStack#initialize:Array(Pointer(Void)) at ??
0x10750b5f8: *CallStack::new:CallStack at ??
0x107505881: *raise<Exception>:NoReturn at ??
0x107505861: *raise<String>:NoReturn at ??
0x1075053b8: __crystal_main at ??
0x10750a858: main at ??
~ ➤ crystal run --release assert.cr
~ ➤
Sija commented 6 years ago

@asterite @oprypin how about adding assert macro to stdlib?

oprypin commented 6 years ago

Yep, I'm definitely all for it

oprypin commented 6 years ago

The proposal is not very convincing though. This is not for unittests. The assert can be sprinkled throughout the code for sanity checks. "This definitely should be true, if it's not, something went totally wrong". Checks that you don't necessarily have to write and that are good for leaving out in release mode.

al6x commented 4 years ago

+1, would be nice to have in stdlib, and during release too. Maybe leave ability to disable it in special circumstances

{% unless flag?(:ludicrous_speed) %}
konovod commented 4 years ago

There is a little need in macro if it used in release - you can just do raise "My custom assertion message" if exp. And using it only in debug is a bad practice imho - release is a place where extra checks won't hurt (except small portion of time-critical code).

Another thing is that it can modify control flow (type of exp was T? without check and T with check). It is unlikely that it can cause bugs though, most likely code would just throw compile-time error.

straight-shoota commented 1 month ago

I've been reminded about this again while working on a couple of algorithms where it would be nice to state some invariants in code.

In my understanding, the main use case would be for developing a feature or chasing a bug. So these assertions would not even need to execute in development builds (without --release), or even in specs. All known invariants should be covered by explicit tests, not inline assertions. They could be enabled by default in non-release builds, though. Details to figure out while we go...

My point is, I see it basically as a development tool which can help me while actively working on a piece of code. They're persisted, so I can easily enable them when needed.

Maybe even the best about it is the documentation function. An invariant clearly expresses the assumptions that the author has about the state of things. Even without executing, it's great as an unambiguous expression, which would otherwise just be a comment. Using a macro and actual code allows running it with the effect of validating whether it's actually true. Besides discorvering bugs in the implementation, over time the invariant might also fall out of sync with the implementation.

Rust has two macros for this purpose:

My main intention would align mostly with debug_assert!. Although I figure the semantics of assert! could also be handy.