1.6.2 Compilation error

This code compiles in 1.6.1 without errors:

abstract class Migration
  module Meta
    include Comparable(Meta)

    abstract def timestamp : String

    def <=>(other : self)
      timestamp <=> other.timestamp
    end
  end

  macro inherited
    def self.timestamp : String
      "dontcare"
    end
  end

  extend Meta

  def self.all : Array(Migration.class)
    [M1] of Migration.class
  end
end

class M1 < Migration
end

Migration.all.sort

See carc.in

But with 1.6.2 it fails with this compilation error:

 199 | v = v1 <=> v2
                  ^-
Error: expected argument #1 to 'M1.<=>' to be Migration::Meta, not Migration+.class

Overloads are:
 - Migration::Meta#<=>(other : self)

The fix is to change the <=> signature:

def <=>(other : self | Migration.class) # here
  timestamp <=> other.timestamp
end

I don’t know why exactly the error occurs and can’t tell whether this is expected behavior or not, so i’m asking here first (My other projects compile and run fine btw, this is the only issue i found).

Reduced:

class Migration
  module Meta
    def foo(other : self)
    end
  end

  extend Meta
end

class M1 < Migration
end

M1.foo(M1) # Error: no overload matches 'M1.foo' with type M1.class

This already fails on 1.6.1 (and earlier), but it can be worked around by an explicit cast to the virtual metaclass:
M1.as(Migration.class).foo(M1.as(Migration.class))

On 1.6.2 this work around does not work. So I guess 1.6.2 broke one edge case that happened to work before, while other similar cases did not even work.

1 Like

The culprit is Fix `VirtualMetaclassType#implements?` to ignore base_type by straight-shoota · Pull Request #12632 · crystal-lang/crystal · GitHub

1 Like

With the revert of #12632 the workaround works. (Note: there’s a typo in your workaround, the second .as(...) should go inside the parens).

self in restrictions always refers to the instance type of the type where the def is instantiated, so this example is not supposed to work; the restriction must be Meta itself. The same goes for #<=> in the original example.

2 Likes

You’re right, the current behavior seems correct then.

There are two possible fixes to the original code: change self with self.class in Meta, or change everything to be Meta as in:

abstract class Migration
  module Meta
    include Comparable(Meta)

    abstract def timestamp : String

    def <=>(other : Meta)
      timestamp <=> other.timestamp
    end
  end

  macro inherited
    def self.timestamp : String
      "dontcare"
    end
  end

  extend Meta

  def self.all : Array(Meta)           
    [M1] of Meta           
  end
end

class M1 < Migration
end

Migration.all.sort
1 Like

Alternatively, you can parametrize Meta and then extend Meta(Migration.class).