Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unsafe_as and as behave differently for certain (valid) typecasts #11058

Open
shinzlet opened this issue Aug 3, 2021 · 2 comments
Open

unsafe_as and as behave differently for certain (valid) typecasts #11058

shinzlet opened this issue Aug 3, 2021 · 2 comments

Comments

@shinzlet
Copy link

shinzlet commented Aug 3, 2021

Bug Report

In certain contexts, #unsafe_as will cast correctly while #as has no effect. More discussion can be found here.

Code

module Readable
  abstract def get_chunk : Readable
end

class ReadableIterator
  include Iterator(Readable)
  
  def initialize(@src : Readable)
  end
  
  def next
    return stop if Random.rand < 0.1
    # @src.get_chunk.unsafe_as(Readable) # This cast works fine
    # @src.get_chunk.as(Readable) # This produces a compiler error, because #next is returning `FooBase+`, not `Readable`
  end
end

class FooBase
end

class ReadonlyFoo < FooBase
  include Readable
  
  def get_chunk : self
    ReadonlyFoo.new
  end
end

class ReadWriteFoo < FooBase
  include Readable
  
  def get_chunk : self
    ReadWriteFoo.new
  end
end

readonly = ReadonlyFoo.new
rw = ReadWriteFoo.new
ReadableIterator.new(readonly).to_a

Error (only occurs when #as is used)

$ crystal run example.cr --error-trace
In example.cr:39:32

 39 | ReadableIterator.new(readonly).to_a
                                     ^---
Error: instantiating 'ReadableIterator#to_a()'


In /usr/lib/crystal/enumerable.cr:1712:5

 1712 | each { |e| ary << e }
        ^---
Error: instantiating 'each()'


In /usr/lib/crystal/iterator.cr:597:7

 597 | yield value
       ^
Error: argument #1 of yield expected to be Readable, not FooBase+

System Information

$ crystal -v
Crystal 1.1.1 (2021-07-26)

LLVM: 10.0.1
Default target: x86_64-pc-linux-gnu
$ uname -r
5.13.6-arch1-1
@HertzDevil
Copy link
Contributor

It seems #10905 introduced the compiler error when .as(Readable) is present, but reverting that PR produces a different error:

In src/enumerable.cr:1712:20

 1712 | each { |e| ary << e }
                       ^-
Error: no overload matches 'Array(Readable)#<<' with type FooBase+

Overloads are:
 - Array(T)#<<(value : T)
Couldn't find overloads for these types:
 - Array(Readable)#<<(value : FooBase)

Something to consider:

x = readonly || rw
y = x.as(Readable)
z = readonly.as(Readable) || rw.as(Readable)

typeof(x) # => FooBase
typeof(y) # => (ReadWriteFoo | ReadonlyFoo)
typeof(z) # => Readable

typeof(!x.is_a?(Iterator::Stop) ? x : raise("")) # => FooBase
typeof(!y.is_a?(Iterator::Stop) ? y : raise("")) # => FooBase
typeof(!z.is_a?(Iterator::Stop) ? z : raise("")) # => Readable

The to_a call would work in spite of #10905 if the fifth typeof here produces either ReadWriteFoo | ReadonlyFoo or Readable. The problem is x.as(Readable) is not an upcast but a sidecast, since the virtual type appears before the cast, and not every FooBase is a Readable.

This suggests one possible workaround:

class ReadonlyFoo < FooBase
  include Readable
  
  def get_chunk : Readable
    ReadonlyFoo.new.as(Readable)
  end
end

class ReadWriteFoo < FooBase
  include Readable
  
  def get_chunk : Readable
    ReadWriteFoo.new.as(Readable)
  end
end

class ReadableIterator
  include Iterator(Readable)
  
  def initialize(@src : Readable)
  end
  
  def next
    return stop if Random.rand < 0.1
    @src.get_chunk # cast not needed
  end
end

typeof(begin
  x = uninitialized Readable
  x.get_chunk
end) # => Readable

@HertzDevil
Copy link
Contributor

More generally:

module A; end

class B; end

class C < B
  include A
end

class D < B
  include A
end

C <= A           # => true
D <= A           # => true
Union(C, D) <= A # => false

If irreducible intersection types are implemented (#2404) then the union of C and D would ideally be B & A, not just B, and the last line above will return true.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants