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