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!
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 
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 raise
s. 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
And I really love Crystal!
3 Likes