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

Union Type on Module is Incorrectly Rejected by Compiler #13243

Open
Capital-EX opened this issue Mar 29, 2023 · 3 comments
Open

Union Type on Module is Incorrectly Rejected by Compiler #13243

Capital-EX opened this issue Mar 29, 2023 · 3 comments

Comments

@Capital-EX
Copy link

System Info

OS: Arch Linux
Crystal Version: 1.7.3
LLVM: 14.0.6
Defaulttarget: x86_64-pc-linux-gnu

Bug Report

When preforming type unions on modules, the crystal compiler will reject the following code:

module A end 
module B end

struct Foo ; include A ; include B ; end
array = [Foo.new]

def g(xs : Array(A | B)) end

# This is fine
ab_list : Array(A|B) = array.select A | B

# This errors
g ab_list

Additionally, it produces that unusual error message that Crystal "expected argument #1 to 'g' to be Array(A | B), not Array(A | B)". This is despite the fact that ab_list is of type Array(A|B). There is nothing in the docs that says this code is invalid. So, I'm lead to believe there is a bug in how the compiler resolves unions of modules.

@straight-shoota
Copy link
Member

It's definitely a bug because the error message doesn't make sense.

Btw. you get the same error message with g array, only here the claim that the argument type is Array(A | B) is wrong because it's Array(Foo).

@Capital-EX
Copy link
Author

One workaround I've found is discarding the module type for the includer types. This can be done at using map(&.itself) or using a macro such as:

macro includes(call)
  {%
    types_to_resolve = [call]
    types = { } of Nil => Nil


    types_to_resolve.each do |t|
      if t.is_a?(Call)
        types_to_resolve << t.receiver
        t.args.each { |tv| types_to_resolve << tv }
      else
        t.resolve.includers.each { |ti|
          types[ti] = true
        }
      end
    end
  %}

  {{ "Union(#{types.keys.splat})".id }}
end

ab_list = array.select(includes A|B)

It's not ideal. But in case someone finds this issue, having a workaround may be useful.

@Capital-EX
Copy link
Author

I'm not sure if I should make a new issue, however I was fiddling with some code after reading #2404 and found a similar bug to the one I reported here:

module A; end
module B; end
module C; end

class ClassA; include A; end
class ClassB; include B; end
class ClassC; include C; end

class ClassAB; include A; include B; end
class ClassAC; include A; include C; end
class ClassBC; include B; include C; end

# In /usr/lib/crystal/pointer.cr:132:29
#
# 132 | (self + offset).value = value
#                               ^----
# Error: type must be (ClassAB | ClassABC | ClassAC | ClassBC | ClassC), not (C | ClassAB)
ys = Array(A|B|C).new.compact_map{|x|
  x if (x.is_a? A && x.is_a? B) || x.is_a? C
}

# Okay, `itself` changes `C` into one of its individual members
zs = Array(A|B|C).new.compact_map{|x|
  x.itself if (x.is_a? A && x.is_a? B) || x.is_a? C
}

It seems like itself will discard the module type in favor of its subtypes. You can see this when using map and typeof:

ys = Array(A|B|C).new.map{|x|
  x if (x.is_a? A && x.is_a? B) || x.is_a? C
}

# Okay, `itself` changes `C` into one of its individual members
zs = Array(A|B|C).new.map{|x|
  x.itself if (x.is_a? A && x.is_a? B) || x.is_a? C
}

puts typeof(ys) # => Array(C | ClassAB | Nil)
puts typeof(zs) # => Array(ClassAB | ClassABC | ClassAC | ClassBC | ClassC | Nil)

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

3 participants