Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions spec/compiler/semantic/while_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,80 @@ describe "Semantic: while" do
)) { nilable int32 }
end

it "doesn't use type at end of endless while if variable is reassigned" do
assert_type(%(
while true
a = 1
if 1 == 1
break
end
a = 'x'
end
a
)) { int32 }
end

it "doesn't use type at end of endless while if variable is reassigned (2)" do
assert_type(%(
a = ""
while true
a = 1
if 1 == 1
break
end
a = 'x'
end
a
)) { int32 }
end

it "doesn't use type at end of endless while if variable is reassigned (3)" do
assert_type(%(
a = {1}
while true
a = a[0]
if 1 == 1
break
end
a = {'x'}
end
a
)) { union_of(int32, char) }
end

it "uses type at end of endless while if variable is reassigned, but not before first break" do
assert_type(%(
while true
if 1 == 1
break
end
a = 1
if 1 == 1
break
end
a = 'x'
end
a
)) { nilable union_of(int32, char) }
end

it "uses type at end of endless while if variable is reassigned, but not before first break (2)" do
assert_type(%(
a = ""
while true
if 1 == 1
break
end
a = 1
if 1 == 1
break
end
a = 'x'
end
a
)) { union_of(int32, char, string) }
end

it "rebinds condition variable after while body (#6158)" do
assert_type(%(
class Foo
Expand Down
38 changes: 30 additions & 8 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2139,8 +2139,28 @@ module Crystal

# If the loop is endless
if endless
after_while_var.bind_to(while_var)
after_while_var.nil_if_read = while_var.nil_if_read?
# Suppose we have
#
# x = exp1
# while true
# x = exp2
# break if ...
# x = exp3
# break if ...
# x = exp4
# end
#
# Here the type of x after the loop will never be affected by
# `x = exp4`, because `x = exp2` must have been executed before the
# loop may exit at the first break. Therefore, if the x right before
# the first break is different from the last x, we don't use the
# latter's type upon exit (but exp2 itself may depend on exp4 if it
# refers to x).
break_var = all_break_vars.try &.dig?(0, name)
unless break_var && !break_var.same?(while_var)
after_while_var.bind_to(while_var)
after_while_var.nil_if_read = while_var.nil_if_read?
end
else
# We need to bind to the variable *before* the condition, even
# after before the variables that are used in the condition
Expand All @@ -2159,21 +2179,23 @@ module Crystal
# outside it must be nilable, unless the loop is endless.
else
after_while_var = MetaVar.new(name)
after_while_var.bind_to(while_var)
nilable = false

if endless
break_var = all_break_vars.try &.dig?(0, name)
unless break_var && !break_var.same?(while_var)
after_while_var.bind_to(while_var)
end

# In an endless loop if not all variable with the given name end up
# in a break it means that they can be nilable.
# Alternatively, if any var that ends in a break is nil-if-read then
# the resulting variable will be nil-if-read too.
if !all_break_vars.try(&.all? &.has_key?(name)) ||
all_break_vars.try(&.any? &.[name]?.try &.nil_if_read?)
nilable = true
after_while_var.nil_if_read = true
end
else
nilable = true
end
if nilable
after_while_var.bind_to(while_var)
after_while_var.nil_if_read = true
end

Expand Down