Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiler: fix incorrect var type insisde nested exception handler #11928

Merged
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
80 changes: 80 additions & 0 deletions spec/compiler/semantic/exception_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -686,4 +686,84 @@ describe "Semantic: exception" do
end
CR
end

it "correctly types variable assigned inside nested exception handler (#9769)" do
assert_type(%(
int = 1
begin
begin
int = "a"
rescue
end
rescue
end
int
)) { union_of(int32, string) }
end

it "types a var after begin rescue as having all possible types and nil in begin if read (2)" do
assert_type(%(
begin
a = 2
a = 'a'
rescue
end
a
)) { union_of [int32, char, nil_type] of Type }
end

it "types a var after begin rescue as having all possible types in begin and rescue" do
assert_type(%(
a = 1.5
begin
a = 2
a = 'a'
a = "hello"
rescue ex
a = false
end
a
)) { union_of [float64, int32, char, string, bool] of Type }
end

it "types a var after begin rescue as having all possible types in begin and rescue (2)" do
assert_type(%(
b = 2
begin
a = 2
a = 'a'
a = "hello"
rescue ex
b = a
end
b
)) { union_of [int32, char, string, nil_type] of Type }
end

it "types a var after begin rescue with no-return in rescue" do
assert_type(%(
lib LibC
fun exit : NoReturn
end

begin
a = 2
a = 'a'
a = "hello"
rescue ex
LibC.exit
end
a
)) { string }
end

it "types a var after rescue as being nilable" do
assert_type(%(
begin
rescue
a = 1
end
a
)) { nilable int32 }
end
end
66 changes: 0 additions & 66 deletions spec/compiler/semantic/ssa_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -214,72 +214,6 @@ describe "Semantic: ssa" do
)) { char }
end

it "types a var after begin rescue as having all possible types and nil in begin if read (2)" do
assert_type(%(
begin
a = 2
a = 'a'
rescue
end
a
)) { union_of [int32, char, nil_type] of Type }
end

it "types a var after begin rescue as having all possible types in begin and rescue" do
assert_type(%(
a = 1.5
begin
a = 2
a = 'a'
a = "hello"
rescue ex
a = false
end
a
)) { union_of [float64, int32, char, string, bool] of Type }
end

it "types a var after begin rescue as having all possible types in begin and rescue (2)" do
assert_type(%(
b = 2
begin
a = 2
a = 'a'
a = "hello"
rescue ex
b = a
end
b
)) { union_of [int32, char, string, nil_type] of Type }
end

it "types a var after begin rescue with no-return in rescue" do
assert_type(%(
lib LibC
fun exit : NoReturn
end

begin
a = 2
a = 'a'
a = "hello"
rescue ex
LibC.exit
end
a
)) { string }
end

it "types a var after rescue as being nilable" do
assert_type(%(
begin
rescue
a = 1
end
a
)) { nilable int32 }
end

it "doesn't change type to nilable inside if" do
assert_type("
def foo
Expand Down
20 changes: 11 additions & 9 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ module Crystal
# Here we store the cumulative types of variables as we traverse the nodes.
getter meta_vars : MetaVars
property is_initialize : Bool
property exception_handler_vars : MetaVars? = nil
property all_exception_handler_vars : Array(MetaVars)? = nil

private enum BlockKind
None
Expand Down Expand Up @@ -535,7 +535,7 @@ module Crystal
def check_exception_handler_vars(var_name, node)
# If inside a begin part of an exception handler, bind this type to
# the variable that will be used in the rescue/else blocks.
if exception_handler_vars = @exception_handler_vars
@all_exception_handler_vars.try &.each do |exception_handler_vars|
var = (exception_handler_vars[var_name] ||= MetaVar.new(var_name))
var.bind_to(node)
end
Expand Down Expand Up @@ -1072,7 +1072,7 @@ module Crystal
block_visitor.fun_literal_context = @fun_literal_context
block_visitor.parent = self
block_visitor.with_scope = node.scope || with_scope
block_visitor.exception_handler_vars = @exception_handler_vars
block_visitor.all_exception_handler_vars = @all_exception_handler_vars
block_visitor.file_module = @file_module

block_scope = @scope
Expand Down Expand Up @@ -2643,17 +2643,21 @@ module Crystal
end

def visit(node : ExceptionHandler)
old_exception_handler_vars = @exception_handler_vars

# Save old vars to know if new variables are declared inside begin/rescue/else
before_body_vars = @vars.dup

# Any variable assigned in the body (begin) will have, inside rescue
# blocks, all types that were assigned to them, because we can't know at which
# point an exception is raised.
# We have a stack of these, to take into account nested exception handlers.
all_exception_handler_vars = @all_exception_handler_vars ||= [] of MetaVars

# We create different vars, though, to avoid changing the type of vars
# before the handler.
exception_handler_vars = @exception_handler_vars = @vars.dup
exception_handler_vars = @vars.dup

all_exception_handler_vars.push exception_handler_vars

exception_handler_vars.each do |name, var|
new_var = new_meta_var(name)
new_var.nil_if_read = var.nil_if_read?
Expand All @@ -2665,7 +2669,7 @@ module Crystal

after_exception_handler_vars = @vars.dup

@exception_handler_vars = nil
all_exception_handler_vars.pop

if node.rescues || node.else
# Any variable introduced in the begin block is possibly nil
Expand Down Expand Up @@ -2788,8 +2792,6 @@ module Crystal
end
end

@exception_handler_vars = old_exception_handler_vars

false
end

Expand Down