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

Wrong type of value of union type after assignment in if clause with cast #12661

Closed
atlantis opened this issue Oct 25, 2022 · 7 comments · Fixed by #12668
Closed

Wrong type of value of union type after assignment in if clause with cast #12661

atlantis opened this issue Oct 25, 2022 · 7 comments · Fixed by #12668

Comments

@atlantis
Copy link

atlantis commented Oct 25, 2022

First of all, thanks for a great language! I'm having issues with some sort of dark compiler type magic when using Hashes in a parameter parsing library:

Hash{"stuff" => "bla", "more" => false, "level" => 33}.select{|k, v| k == "level"}.each do |key, param_value|
  puts "BEFORE class for #{key}: #{param_value.class.name}"  
  if as_i = param_value.as?(Int32)
    puts "YES #{as_i} was truthy"
  end

  if as_s = param_value.as?(String)
    puts "Made it string #{as_s}.. never happens"
  elsif as_i = param_value.as?(Int32)
    puts "Made it int #{as_i}.. what should happen"
  elsif as_b = param_value.as?(Bool)
    puts "Made it bool #{as_b}... what the heck?!"
  else
    puts "No dice"
  end

  puts "AFTER class for #{key}: #{param_value.class.name}"
end

produces

BEFORE class for level: Int32
YES 33 was truthy
Made it bool true... what the heck?!
AFTER class for level: Int32

However, if I change the Hash to only Hash{"level" => 33} it works as expected. What's odd is that at the beginning of the loop I confirm that param_value.as?(Int32) is truthy, and at the very end of the loop the value is still Int32, yet somehow param_value.as?(Int32) fails to pick it up?

Let me know if there's anything else I can do to help (other than hacking the compiler, for which I'm unfortunately not qualified :)

@atlantis
Copy link
Author

Oh, and I discovered this in Crystal 1.0 and confirmed at https://play.crystal-lang.org/#/r/dy5j that it's still present in 1.6.1, so it appears to be deeply entrenched.

@atlantis
Copy link
Author

atlantis commented Oct 25, 2022

Sorry, I should also add that if I remove the if as_s = param_value.as?(String) logic fork like so:

Hash{"stuff" => "bla", "more" => false, "level" => 33}.select{|k, v| k == "level"}.each do |key, param_value|
  puts "BEFORE class for #{key}: #{param_value.class.name}"  
  if as_i = param_value.as?(Int32)
    puts "YES #{as_i} was truthy"
  end
 
  if as_i = param_value.as?(Int32)
    puts "Made it int #{as_i}.. what should happen"
  elsif as_b = param_value.as?(Bool)
    puts "Made it bool #{as_b}... what the heck?!"
  else
    puts "No dice"
  end
 
  puts "AFTER class for #{key}: #{param_value.class.name}"
end

Then everything works properly. So something about the if as_s = param_value.as?(String) line seems to be actually changing the type of param_value somehow... but not really changing it since the type is fine again when we get to the AFTER.

@Blacksmoke16
Copy link
Member

Blacksmoke16 commented Oct 25, 2022

Reduced:

val = 33 || false || "foo"

if as_s = val.as?(String)
  puts "Made it string #{as_s}.. never happens"
elsif as_i = val.as?(Int32)
  puts "Made it int #{as_i}.. what should happen"
elsif as_b = val.as?(Bool)
  puts "Made it bool #{as_b}... what the heck?!"
else
  puts "No dice"
end

Printing val.as? Int32 does print 33, but seems to be nil when it comes to the other test. It also seems to work if you have 3 separate if statements, so something in the elsif must be throwing it off.

@atlantis This does seem quite strange, tho I'm not sure as? for checking types is what you actually want. I would switch to using .is_a?. Something like:

if param_value.is_a?(String)
  puts "Made it string #{param_value}.. never happens"
elsif param_value.is_a?(Int32)
  puts "Made it int #{param_value}.. what should happen"
elsif param_value.is_a?(Bool)
  puts "Made it bool #{param_value}... what the heck?!"
else
  puts "No dice"
end

Ref: https://crystal-lang.org/reference/1.6/syntax_and_semantics/is_a.html

@atlantis
Copy link
Author

Yes fortunately it's easy to work around (i used a case and it works fine) but I wanted you guys to know about it cause it seems the sort of thing that might generate very subtle, low-level bugs! Thanks for all your work!

@straight-shoota
Copy link
Member

straight-shoota commented Oct 25, 2022

Reduced further:

val = 33.as(Bool | Int32 | String)

if a = val.as?(String)
else
  typeof(val) # => Bool
end

The problem seems to be caused by the assignment in the if condition. If you pull that out, it works as expected:

val = 33.as(Bool | Int32 | String)

a = val.as?(String)
if a
else
  typeof(val) # => (Bool | Int32 | String)
end

@jzakiya
Copy link

jzakiya commented Oct 25, 2022

What happens if you do this?

if (a = val.as?(String))
else
  typeof(val) # => Bool
end

@straight-shoota
Copy link
Member

Same. Parentheses don't change anything.

@beta-ziliani beta-ziliani changed the title Hash union type confusion Wrong type of value of union type after assignment in if clause with cast Oct 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants