opened
asterite
username_0
Fixes #6996

Consider this code:

```crystal
abstract class Foo
end

foos = [] of Foo
foos.all?(&.valid?) # => true
```

The code above compiles fine in Crystal. The reason is that, because `Foo` is abstract, only its subclasses can actually be instantiated. But there are no subclasses! So there's no way to actually instantiate a `Foo`.

The compiler will then consider any call on such abstract types to be valid, but produce `NoReturn`... which makes sense: there's no way to reach that (but see the "Reaching that NoReturn" at the end).

The reason this also compiles is because Crystal tries to mimic Ruby. Let's consider this Ruby code:

```ruby
class Foo
end

foos = [] of Foo
foos.all?(&:valid?) # => true
```

It works! Because we didn't pass any Foo, it's trivially true. Ruby doesn't even need to check that `valid?` method, **and Crystal does the same thing**.

Crystal is always "if you don't call it, we don't know". Which makes sense according to its prototypey and duck-typey nature.

## But it didn't always work

If `Foo` had an abstract subclasses, the compiler used to complain:

```crystal
abstract class Foo
end

abstract class Bar < Foo
end

foos = [] of Foo
foos.all?(&.valid?) # Error: undefined method 'valid?' for Foo+
```

The bug is that the compiler just checked whether `Foo` was abstract without subclasses. But the condition should also apply if `Foo` is abstract and all of its subclasses are in the same condition. The compiler calls this "abstract leaf".

It can also happen with generic types, even if they are concrete:

```crystal
abstract class Foo
end

class Bar(T) < Foo
end

foos = [] of Foo
foos.all?(&.valid?) # => true
```

The above snippet didn't work, but with this PR it works. It works because `Bar(T)` has no generic instances. `Foo` was never actually instantiated.

And if you do instantiate `Bar` you'll get an error saying "undefined method 'valid?' for Bar(Int32) (compile-time type is Foo+)".

## Why is this useful?

This is useful because one could define a hierarchy of abstract types, or generic types, and write code to work with that. Maybe a test creates instances. Maybe other tests don't because they expect third party shards to defined subclasses. All of these cases should work.

## Reaching that NoReturn

Actually, there is:

```crystal
abstract class Foo
end

ptr = Pointer(Foo).malloc(1)
ptr.value.valid?
```

The above compiles fine and raises an error on runtime:

```
can't execute `ptr.value.valid?` at /Users/username_0/Projects/crystal/foo.cr:5:3: `ptr.value.valid?` has no type (Exception)
```

But that's unsafe and it usually never happens in real code: the pointer is inside an array, there are bound checks, etc.
Compiler: don't say "undefined method" on abstract leaf types
issue
created
yxhuvud
username_1
What I don't understand about the explanation is why `malloc` is allowed to allocate an abstract class. What does that even mean?
comment
created
asterite
username_0
It's not allocating an abstract class. It's allocating space to put an abstract class.
comment
created
asterite
username_0
Better said: space to put anything that inherits from that abstract class.
comment
created
yxhuvud
username_1
Ah ok, and it only works because it references have a fix size. Got it.
comment
created
j8r
username_2
I hoped it fixes https://github.com/crystal-lang/crystal/issues/8853 too, but actually no.
comment
created
bew
username_3
In my opinion, the first code should NOT work, unless you declare an abstract def for the method, so:
```cr
abstract class Foo
  # Without this is should fail to compile
  abstract def valid? : Bool
end

foos = [] of Foo
foos.all?(&.valid?) # => true
```

no?
comment
created
asterite
username_0
@username_3 Ideally, maybe yes. Abstract methods are not required at all, they are just there to better document the code. The compiler doesn't use abstract methods at all except for checking that subclasses implement them. But it's a different topic.
comment
created
bew
username_3
But then, how `foos.all?(&.valid?)` is supposed to work type-wise if `valid?` isn't declared anywhere?
comment
created
asterite
username_0
The compiler says: there are no concrete instances. It doesn't matter what this method is or how is this called, I'm going to type it as NoReturn.
comment
created
bew
username_3
@username_0 I see thanks!
comment