Incorrect overload selected with freevar and generic inheritance

Possibly related to Incorrect overload selected for Array(Array(T)) vs Array(T) · Issue #8973 · crystal-lang/crystal · GitHub (or at least one of the linked tickets).

abstract struct ParamConverterInterface
  abstract struct ConfigurationInterface
    getter converter : ParamConverterInterface.class

    def initialize(@converter : ParamConverterInterface.class); end
  end

  abstract struct ArgAwareConfiguration(ArgType) < ConfigurationInterface
    getter type : ArgType.class = ArgType
  end

  def apply(configuration) : Nil
    pp "DEFAULT #{configuration.type}"
  end
end

struct P1 < ParamConverterInterface
  struct Configuration(ArgType) < ArgAwareConfiguration(ArgType); end

  def apply(configuration : Configuration(T)) : Nil forall T
    pp "P1 CORRECT"
  end
end

struct P2 < ParamConverterInterface
  struct Configuration(ArgType) < ArgAwareConfiguration(ArgType); end

  def apply(configuration : Configuration(String)) : Nil
    pp "P2 STRING"
  end

  def apply(configuration : Configuration(B)) : Nil forall B
    pp "P2 FALLBACK #{B}"
  end
end

converters = Hash(ParamConverterInterface.class, ParamConverterInterface).new
converters[P1] = P1.new
converters[P2] = P2.new

configuration = [
  P1::Configuration(Int32).new(P1),
  P2::Configuration(String).new(P2),
  P2::Configuration(Bool).new(P2),
]

configuration.each do |configuration|
  converters[configuration.converter].apply configuration
end

https://play.crystal-lang.org/#/r/bsqd

I would have expected it to print:

"P1 CORRECT"
"P2 STRING"
"P2 FALLBACK Bool"

But it seems the the default overload from the parent type is being chosen over the forall B in the P2 method. HOWEVER, removing the ArgAwareConfiguration and moving the generic into the base ConfigurationInterface resolves the issue. So I’m not sure if this is a bug, or expected because of the internals of free vars/generic inheritance.

https://play.crystal-lang.org/#/r/bsqf

I think the example where moving the generic into ConfigurationInterface is a red herring. Adding a third P2::Configuration to the array of type Float64 forces the float overload to use use the default, instead of the fallback with the free var as the free var is probably “consumed” via the Bool type.

Was able to workaround this for my use case by storing the configuration types in a tuple, then using a macro loop to call the #apply method. I guess this way the compiler is better able to know what is going on.

converters = Hash(ParamConverterInterface.class, ParamConverterInterface).new
converters[P1] = P1.new
converters[P2] = P2.new
 
configuration = {
  P1::Configuration(Int32).new(P1),
  P2::Configuration(String).new(P2),
  P2::Configuration(Bool).new(P2),
}
 
{% for idx in (0..2) %}
  %config = configuration[{{idx}}]
  converters[%config.converter].apply %config
{% end %}

https://play.crystal-lang.org/#/r/btaf

I’m working on a library that uses the visitor pattern. I have a base Expression class that is abstract and several classes that extend it. Some have generic types. I’m finding that I can’t use generic types in the visit method and have to be explicit with the type or it fails. Has anyone come across this issue before? I’d move away from the indirection but it’s necessary for this library.

Here’s a minimal example: Carcin
Notice that the generic method works for Int64 but fails on Int32 but only after putting it in the expression list. Using the visitor directly from the class works as expected.

From @HertzDevil

you could put {% @type %} inside Expression#accept to force the compiler to instantiate a separate def for each generic instance

https://play.crystal-lang.org/#/r/ijy8

This worked for me