Run time assertions and type reductions

This is a bit related to Generic compile time assertions but on the runtime side:

If I use the plain raise as in the example, it works as expected, the compile time type gets reduced and the code compiles.
However, if I encapsulate it in the assert function (and comment out the raise), it doesn’t have this effect anymore and doesn’t compile, since Bool doesn’t have a + defined. Is there a way to assist the compiler on this?

def assert(invariant : Bool)
    raise("runtime_assert") if !invariant
end

def fn(arg : Bool|Int32)
    # p typeof(arg) # Bool|Int32
    raise("sth") if arg.is_a?(Bool) # ensures that "arg+42" can be calculated, i.e. reduces compile-time type to Int32; without we get a compile-time error
    # assert(!arg.is_a?(Bool)) # doesn't help, always a compile-time error
    arg+42
end

a = [1,true]
p fn(a[0])

Thanks again!

No

You could prob make it be a macro?

macro assert(invariant)
  raise("runtime_assert") if !{{invariant}}
end

Would expand to what you have now, but still give you the same syntax.

Another option would be to use overloads more. I.e. have two fn methods that accept one of the types in the union, then you wouldn’t need this check at all.

2 Likes

Oh, I said no because I though they wanted a function, not a macro

Just for the record:

It gets a bit more complex due to the use case assert(false).
The above implementation throws this compiler error…

Error: method top-level x must return Bool but it is returning Nil

… for this sample:

# doesn't compile
macro assert(invariant)
    raise("runtime_assert") if !{{invariant}}
end

# does compile
# macro assert(invariant)
#     {% if invariant == false %}
#         raise("runtime_assert")
#     {% else %}
#         raise("runtime_assert") if !{{invariant}}
#     {% end %}
# end

def x : Bool
    assert(false) # doesn't compile with the first implementation
    # raise("xx") # does compile
end

x

Yea good call, could simplify it to:

macro assert(invariant)
  {% unless invariant %}
    raise("runtime_assert")
  {% end %}
end

As we want to do the check at compile time, and if the call is truthy, then have the macro not output anything, versus checking at runtime.

Now you’re mixing up compile time and runtime. This version of assert is supposed to check the invariant at runtime (opposed to Generic compile time assertions).
Nevertheless, it also reduced types on compile time, as raise does in the initial example.

Your last proposal doesn’t work for the initial example:

# doesn't compile
macro assert(invariant)
  {% unless invariant %}
    raise("runtime_assert")
  {% end %}
end

# compiles
# macro assert(invariant)
#     {% if invariant %}
#         raise("runtime_assert") if !{{invariant}}
#     {% else %}
#         raise("runtime_assert")
#     {% end %}
# end

def fn(arg : Bool|Int32)
    assert(!arg.is_a?(Bool))
    arg+42 # compile-time error in case of the first assert variant, assert doesn't reduce the type
end

a = [1,true]
p fn(a[0])

I’m not sure if the working version can be reduced.

ahh okay yea good call again :sweat_smile:

Just return false from x? It says x must return Bool, but the generated macro code has an empty if else branch. I guess it depends on what you want assert to return.

Hm. I played around a bit more. This looks like a bug to me now:

# doesn't compile; doesn't reduce the _return_ type
macro assert(invariant)
    raise("runtime_assert") if true
end
# compiles (does reduce the return type)
# macro assert(invariant)
#     raise("runtime_assert")
# end

def fn(arg)
    if arg.is_a?(Int32)
        arg + 1
    else
        # raise("xx") # compiles
        assert(false) # depends on above implementation
    end
end

p fn(42)+1 # the +1 only compiles in some cases, as noted above

What do you think?

Note that if you put raise("xx") if true directly in the else branch it will produce the same compile-time error. Crystal is not smart enough to detect the condition always hold, and just sees an if in which the else case it’s unspecified—a Nil. Unless you’re referring to something different, this is not a bug.

2 Likes

Yes, you’re right, macros are not part of the minimum example just above - I figured it out too late.

The Crystal compiler is already really smart at type reductions on arguments to conditional raises. So it comes as a surprise it doesn’t handle the if true case.

Thanks to all of you, I have what I want anyhow :smiley: And I really love Crystal!

3 Likes