Skip to content
Closed
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
24 changes: 24 additions & 0 deletions spec/compiler/codegen/class_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,30 @@ describe "Code gen: class" do
CRYSTAL
end

it "dispatches virtual metaclass call to uninstantiated generic subclass (#11134)" do
run(<<-CRYSTAL).to_i.should eq(2)
class Foo
def self.foo
1
end
end

class Bar(T) < Foo
def self.foo
2
end
end

class Baz < Foo
def self.foo
3
end
end

Bar.as(Foo.class).foo
CRYSTAL
end

it "can assign virtual metaclass to virtual metaclass (#3007)" do
run(<<-CRYSTAL).to_i.should eq(2)
class Foo
Expand Down
14 changes: 14 additions & 0 deletions spec/compiler/semantic/is_a_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,18 @@ describe "Semantic: is_a?" do
end
CRYSTAL
end

it "matches `is_a?(GenericA) && is_a?(GenericB)` when GenericB <= GenericA (#10831)" do
# Before the fix, `common_descendent(GenericClassType, GenericClassType)`
# only walked one direction, so the chained filter narrowed `x` to `B(T)`
# and `is_a?(C)` returned `false` for a runtime value that's actually a C.
run(<<-CRYSTAL).to_b.should be_true
class A; end
class B(T) < A; end
class C(T) < B(T); end

x = C(Int32).new.as(A)
x.is_a?(B) && x.is_a?(C)
CRYSTAL
end
end
9 changes: 7 additions & 2 deletions src/compiler/crystal/semantic/method_lookup.cr
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,13 @@ module Crystal
matches = base_type_matches.matches
changes = nil

# Traverse all subtypes
instance_type.subtypes(base_type).each do |subtype|
# Traverse all subtypes. For virtual metaclasses, include uninstantiated
# generic subclasses too — their metaclasses are valid dispatch targets
# (e.g. `Bar.foo` where `class Bar(T) < Foo`), and without them the
# dispatch table is missing a branch and the call hits the unreachable
# at runtime (#11134).
subtypes = virtual_metaclass? ? instance_type.subtypes_including_generic(base_type) : instance_type.subtypes(base_type)
subtypes.each do |subtype|
next if is_new && subtype.is_a?(GenericClassInstanceType) && subtype.abstract?

subtype_lookup = virtual_lookup(subtype)
Expand Down
7 changes: 6 additions & 1 deletion src/compiler/crystal/semantic/type_intersect.cr
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,12 @@ module Crystal
def self.common_descendent(type1 : GenericClassType, type2 : GenericClassType)
return type1 if type1 == type2

common_descendent_instance_and_generic(type1, type2)
# Try both directions: e.g. for `class C(T) < B(T)`, `common_descendent(B, C)`
# should resolve to `C` because every C is also a B. Without the symmetric
# check, chained type filters like `x.is_a?(B) && x.is_a?(C)` collapse to
# `NoReturn` (#10831).
common_descendent_instance_and_generic(type1, type2) ||
common_descendent_instance_and_generic(type2, type1)
end

def self.common_descendent(type1 : Type, type2 : AliasType)
Expand Down
39 changes: 37 additions & 2 deletions src/compiler/crystal/types.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3431,21 +3431,53 @@ module Crystal
subtypes
end

def subtypes(type)
def subtypes(type : Type)
subtypes = [] of Type
type.subclasses.each do |subclass|
collect_subtypes subclass, subtypes
end
subtypes
end

# Like `subtypes`, but also yields uninstantiated generic subclasses.
# Used for virtual *metaclass* dispatch, where `Bar.foo` is callable
# even when `class Bar(T)` has no instantiation (#11134).
def subtypes_including_generic
subtypes = [] of Type
collect_subtypes_including_generic(base_type, subtypes)
subtypes
end

def subtypes_including_generic(type : Type)
subtypes = [] of Type
type.subclasses.each do |subclass|
collect_subtypes_including_generic subclass, subtypes
end
subtypes
end

def collect_subtypes(type, subtypes)
subtypes << type unless type.is_a?(GenericType) || type.unbound?
type.subclasses.each do |subclass|
collect_subtypes subclass, subtypes
end
end

def collect_subtypes_including_generic(type, subtypes)
if type.is_a?(GenericType)
# Only include the uninstantiated generic if it has no instantiations:
# those carry the dispatch otherwise. Including the uninstantiated form
# when instantiations exist can fail later if a class method on it
# references the type parameter (e.g. `def self.x; T.foo; end`).
subtypes << type if type.instantiated_types.empty? && !type.unbound?
elsif !type.unbound?
subtypes << type
end
type.subclasses.each do |subclass|
collect_subtypes_including_generic subclass, subtypes
end
end

def lookup_class_var?(name)
class_var = @class_vars.try &.[name]?
return class_var if class_var
Expand Down Expand Up @@ -3519,7 +3551,10 @@ module Crystal
end

def each_concrete_type(&)
instance_type.subtypes.each do |type|
# Include uninstantiated generic subclasses: their metaclasses are
# callable (e.g. `Bar.foo` where `class Bar(T)`) and must have a
# matching branch in virtual dispatch on `Foo+.class` (#11134).
instance_type.subtypes_including_generic.each do |type|
yield type.metaclass
end
end
Expand Down
Loading