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

Compiler may not assume correct integer literal type from enclosing context #6558

Closed
RomanHargrave opened this issue Aug 18, 2018 · 4 comments

Comments

@RomanHargrave
Copy link

Tested with Crystal 0.25.1 (the currently available version from the official debian repository)

There exist situations where the compiler will not correctly determine the appropriate integer type to use for ambiguously specified integer literals (e.g. lacking the integer type suffix).

Take, for example, the following extension to Slice

struct Slice(T)
   def zero!
      self.map! do |x|
         0
      end
   end
end

Because the compiler appears to assume ambiguous integers to be Int32 when the type is unspecified (or in this case when it gets confused), zero! will compile (and therefore run) as expected when called on Slice[1, 2, 3] (since Int32 is the default type); however, attempting to do the same with Slice[1_u8, 2_u8, 3_u8] will result in some rather misleading compiler errors regarding argument type compatibility for Pointer#[]= called within Slice#map!.

While this could supposedly be all good and well if there existed a means to 'blindly' convert integer types at compile time, no such manner exists. While 0.cast(T) may appear acceptable, it prohibits nearly any conversion - presumably on the basis that Int32 and some other Int have different sizes. These leads me to believe that, In order to support each possible member type for Slice(T) one would have to specialize - by hand - zero! for each possible T, which seems absurd.

I assume that this misbehavior is brought about by inability to constrain the return type of a block, which would explain the compilers unwavering choice of Int32; however, I am not entirely privy to the existence, or lack thereof, of such an inability - and therefore may be entirely wrong in my diagnosis of the problem.

As an aside, I observed that the compiler refuses to allow casts between Int32 and UInt32 which, while I have no use for, i find amusing solely because the types should be compatible at memory level. Yes, I do realize that the sign bit would be treated as part of the value in the event that it were set, but one has to assume that if #to(T) is being used that the developer acknowledges it is now their responsibility to ensure the integrity of the resulting outcome.

@Sija
Copy link
Contributor

Sija commented Aug 18, 2018

Yes, I do realize that the sign bit would be treated as part of the value in the event that it were set, but one has to assume that if #to(T) is being used that the developer acknowledges it is now their responsibility to ensure the integrity of the resulting outcome.

There is Object#unsafe_as existing specifically to cover such cases.

10.unsafe_as(UInt32) # => 10_u32

Also, your case would be better handled with Number.zero, see:

struct Slice(T)
  def zero!
    map! { T.zero }
  end
end

@asterite
Copy link
Member

@Sija unsafe is the last resort. In fact, I'd like to say it's never a solution. In fact, it won't work well, I think, when trying to covert between types of different bit size.

In this case there's a new method too: one can do T.new(0) where T is a numeric type. Of course T. zero works too, and it's implemented using that new method.

@RomanHargrave
Copy link
Author

Thanks @Sija, though that brings up another question - does there exist manual type hinting?

@RX14
Copy link
Contributor

RX14 commented Aug 20, 2018

@RomanHargrave a couple of extra notes: 0 is always an Int32. The compiler doesn't attempt to work out the value of literals from other code, the way the compiler works is to start by typing code literals and then work out types "outwards" from those. That means that each literal must have a single concrete type in crystal. The (recently added) exception to this rule is #6074, and that's only valid for certain very specific, explicitly typed cases. You can manually change the type of the literal using 0_u32 (covered in the docs).

Casting in Crystal is using .as and is used only for casting up and down the type hierarchy. Since Int32 and UInt32 are siblings in the type hierarchy, not in a parent/child relationship, they can't be cast to each other. In usual code you'd use #to_u32 methods, and in generic code you can use T.new to cast - which simply dispatches to the correct #to_{u,i}{size} method as required. unsafe_as is a literal binary reinterpretation of the value and is only rarely useful - hence called unsafe_as because it's unsafe and you can easily cause segfaults with it.

Hope that helps!

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

No branches or pull requests

4 participants