A little convoluted title, but the example will clear things up:
if (x = 1 + 1) > 0
p typeof(x) #=> Int32
end
if true
if (y = 1 + 1) > 0
p typeof(y) #=> Int32
end
end
if true && (z = 1 + 1) > 0
p typeof(z) #=> (Int32 | Nil) -- why??
end
Is this intended? 2nd and 3rd cases are practically the same thing, aren’t they?
Maybe the thread title should be something like “Non-nil variable assigned in complex if has Nil union type in body” ? (not sure if thread titles can have formatted text like that)
Grabbing that last section of code and adding one more line:
if true && (z = 1 + 1) > 0
p typeof(z) #=> (Int32 | Nil) -- why?
end
p typeof(z) #=> (Int32 | Nil) -- sure, yes, seems right
It does make sense to me that the second typeof(z) in my version would return (Int32 | Nil) because I can’t imagine a “real” example where the compiler would know for sure that the left side of that && was true. The conditional above is logically equivalent to if (z = 1 + 1) > 0; the true && really changes nothing to the truthiness. If we used any non-trivial condition instead then it might very well be false, so z would never be assigned.
On the other hand, z will always be assigned in the body of the if. However…
a = do_something(1)
b = do_literally_anything(2)
c = no_seriously_just_do_anything_at_all(3)
if (a > 0 && b < 0) || (c < 10 && (z = 1 + 1) > 0)
p typeof(z) #=> (Int32 | Nil) -- okay, yeah, that's right in this case
end
We can reason through this conditional and see that it’s reasonable to say that z could be unassigned in the if body. But does the compiler need to be able to unravel any possible conditional to determine whether variables assigned in the condition can be Nil in the body? I’m not really sure whether it’s worthwhile or not, but I thought it was worth raising that this could be a complex capability to fully implement and might not be worth the time or additional compiler complexity.
But it’s only when you use it on the expression. If you just assign it, the compiler knows it isn’t nil inside the if-block:
if true && (z = 1 + 1)
p typeof(z) #=> Int32
end
p typeof(z) #=> (Int32 | Nil)
As far as I know, there isn’t any method you can call on a non-nil variable to make it nil, so it shouldn’t matter if you call anything on it after you assign it.
This looks like an omission in the type filter algorithm. It seems to consider assignments only as direct operands of a logical operator, not when it’s nested within a call.
MainVisitor#visit(Call) sets @type_filters = nil. That would explain this behaviour.