Skip to content
Open
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
90 changes: 90 additions & 0 deletions spec/compiler/codegen/case_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,94 @@ describe "Code gen: case" do
end
CRYSTAL
end

it "does case when with a type union, doesn't resolve to virtual type (#9665)" do
run(<<-CRYSTAL).to_i.should eq(2)
class Foo; end
class Bar < Foo; end
class Baz < Foo; end
class Bang < Foo; end

x = Bang.new || Bar.new
case x
when Bar | Baz
1
else
2
end
CRYSTAL
end

it "does case when with a union alias, doesn't resolve to virtual type (#9665)" do
run(<<-CRYSTAL).to_i.should eq(2)
class Foo; end
class Bar < Foo; end
class Baz < Foo; end
class Bang < Foo; end

alias BarBaz = Bar | Baz

x = Bang.new || Bar.new
case x
when BarBaz
1
else
2
end
CRYSTAL
end

it "does case when with a type union, matches a member (#9665)" do
run(<<-CRYSTAL).to_i.should eq(1)
class Foo; end
class Bar < Foo; end
class Baz < Foo; end

x = Bar.new || Baz.new
case x
when Bar | Baz
1
else
2
end
CRYSTAL
end

it "does case when with a flags enum union keeps value semantics (#9665)" do
run(<<-CRYSTAL).to_i.should eq(2)
require "prelude"

@[Flags]
enum Color
Red
Green
Blue
end

x = Color::Green
case x
when Color::Red | Color::Blue
1
else
2
end
CRYSTAL
end

it "does case when with a numeric constant union keeps value semantics (#9665)" do
run(<<-CRYSTAL).to_i.should eq(2)
require "prelude"

A = 1
B = 2

x = 4
case x
when A | B
1
else
2
end
CRYSTAL
end
end
45 changes: 45 additions & 0 deletions spec/compiler/codegen/exception_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,51 @@ describe "Code gen: exception" do
CRYSTAL
end

it "rescues a union alias by its members, not their ancestor (#9665)" do
run(<<-CRYSTAL).to_i.should eq(2)
require "prelude"

class FooErr < Exception; end
class BarErr < FooErr; end
class BazErr < FooErr; end
class QuuxErr < FooErr; end

alias BarBaz = BarErr | BazErr

x = 0
begin
begin
raise QuuxErr.new
rescue BarBaz
x = 1
end
rescue
x = 2
end
x
CRYSTAL
end

it "rescues a member through a union alias (#9665)" do
run(<<-CRYSTAL).to_i.should eq(1)
require "prelude"

class FooErr < Exception; end
class BarErr < FooErr; end
class BazErr < FooErr; end

alias BarBaz = BarErr | BazErr

x = 0
begin
raise BarErr.new
rescue BarBaz
x = 1
end
x
CRYSTAL
end

it "receives exception object" do
run(<<-CRYSTAL).to_string.should eq("Ex1")
require "prelude"
Expand Down
85 changes: 85 additions & 0 deletions spec/compiler/codegen/is_a_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,91 @@ describe "Codegen: is_a?" do
CRYSTAL
end

it "does is_a? with union type as alias, don't resolve to virtual type (#9665)" do
run(<<-CRYSTAL).to_b.should be_false
class A
end

class B < A
end

class C < A
end

class D < A
end

alias BorC = B | C

x = D.new || C.new
x.is_a?(BorC)
CRYSTAL
end

it "does is_a? with union type as alias of Union(X, Y), don't resolve to virtual type (#9665)" do
run(<<-CRYSTAL).to_b.should be_false
class A
end

class B < A
end

class C < A
end

class D < A
end

alias BorC = Union(B, C)

x = D.new || C.new
x.is_a?(BorC)
CRYSTAL
end

it "does is_a? with union type as alias of an alias, don't resolve to virtual type (#9665)" do
run(<<-CRYSTAL).to_b.should be_false
class A
end

class B < A
end

class C < A
end

class D < A
end

alias BorC = B | C
alias BorC2 = BorC

x = D.new || C.new
x.is_a?(BorC2)
CRYSTAL
end

it "does is_a? true for a union alias member's subclass (#9665)" do
run(<<-CRYSTAL).to_b.should be_true
class A
end

class B < A
end

class C < A
end

class SubB < B
end

alias BorC = B | C

x = SubB.new.as(A)
x.is_a?(BorC)
CRYSTAL
end

it "restricts union metaclass to metaclass (#12295)" do
run(<<-CRYSTAL).to_i.should eq(2)
x = true ? Union(String | Int32) : String
Expand Down
48 changes: 48 additions & 0 deletions spec/compiler/semantic/restrictions_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,54 @@ describe "Restrictions" do
CRYSTAL
end

it "restricts against an alias of a union by its members, not their ancestor (#9665)" do
assert_error <<-CRYSTAL, "expected argument #1 to 'foo' to be Foo, not Quux"
class Foo; end
class Bar < Foo; end
class Baz < Foo; end
class Quux < Foo; end

alias BarBaz = Bar | Baz

def foo(x : BarBaz)
end

foo(Quux.new)
CRYSTAL
end

it "restricts against an alias of Union(X, Y) by its members, not their ancestor (#9665)" do
assert_error <<-CRYSTAL, "expected argument #1 to 'foo' to be Foo, not Quux"
class Foo; end
class Bar < Foo; end
class Baz < Foo; end
class Quux < Foo; end

alias BarBaz = Union(Bar, Baz)

def foo(x : BarBaz)
end

foo(Quux.new)
CRYSTAL
end

it "matches a member against an alias of a union restriction (#9665)" do
assert_type(<<-CRYSTAL) { int32 }
class Foo; end
class Bar < Foo; end
class Baz < Foo; end

alias BarBaz = Bar | Baz

def foo(x : BarBaz)
1
end

foo(Bar.new)
CRYSTAL
end

it "should not let GenericChild(Base) pass as a GenericBase(Child) (#1294)" do
assert_error <<-CRYSTAL, "expected argument #1 to 'foo' to be GenericBase(Child), not GenericChild(Base)"
class Base
Expand Down
49 changes: 48 additions & 1 deletion src/compiler/crystal/semantic/literal_expander.cr
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,7 @@ module Crystal
case cond
when NilLiteral
return IsA.new(right_side, Path.global("Nil"))
when Path, Generic
when Path, Generic, Union
return IsA.new(right_side, cond)
when Call
obj = cond.obj
Expand All @@ -937,13 +937,60 @@ module Crystal
else
# no special treatment
end

# A `when Foo | Bar` parses as a `|` call between type-like operands,
# not as a `Union` node. Turn it into an `is_a?` against the union so it
# behaves like `when Foo, Bar` and like `x.is_a?(Foo | Bar)`, instead of
# comparing the union value (its common ancestor) with `===` (#9665).
# If the operands turn out to be constants (e.g. enum members or numeric
# constants) rather than types, the `IsA` is reverted to the original
# `===` comparison during semantic analysis.
if union = case_when_union(cond)
return IsA.new(right_side, union)
end
else
# no special treatment
end

Call.new(cond, "===", right_side)
end

# If *node* is a `|` call chain between type-like operands (`Foo | Bar`,
# `Foo | Bar | Baz`, possibly with `.class` or aliases), returns the
# equivalent `Union` node. Otherwise returns `nil`.
private def case_when_union(node)
return nil unless node.is_a?(Call)
return nil unless node.name == "|" && node.args.size == 1
obj = node.obj
arg = node.args.first
return nil unless obj

left = case_when_union_operand(obj)
return nil unless left
right = case_when_union_operand(arg)
return nil unless right

Union.new(left + right).at(node)
end

# Returns the list of type nodes *node* contributes to a `case ... when`
# union, or `nil` if it isn't a type-like operand.
private def case_when_union_operand(node) : Array(ASTNode)?
case node
when Path, Generic, Union
[node] of ASTNode
when Call
obj = node.obj
if node.name == "class" && node.args.empty? && (obj.is_a?(Path) || obj.is_a?(Generic))
[Metaclass.new(obj).at(obj)] of ASTNode
else
case_when_union(node).try { |union| union.types }
end
else
nil
end
end

macro check_implicit_obj(type)
if cond.is_a?({{type}})
cond_obj = cond.is_a?(Not) ? cond.exp : cond.obj
Expand Down
Loading
Loading