Misleading error message when type is not match

Check following example:

require "socket"

struct Test
  getter host : String?
  getter port : Int32?

  def initialize(@host, @port)
  end
end

test = Test.new("127.0.0.1", 3005)

TCPServer.new(test.host.not_nil!, test.port)

# Following code works
# TCPServer.new(test.host.not_nil!, test.port.not_nil!)

Get following misleading error msg:

In 1.cr:13:25

 13 | TCPServer.new(test.host.not_nil!, test.port)
                              ^-------
Error: expected argument #1 to 'TCPServer.new' to be Int, not String

Overloads are:
 - TCPServer.new(host : String, port : Int, backlog : Int = SOMAXCONN, dns_timeout = nil, reuse_port : Bool = false)
 - TCPServer.new(family : Family = Family::INET)
 - TCPServer.new(port : Int, backlog = SOMAXCONN, reuse_port = false)
 - TCPServer.new(*, fd : Handle, family : Family = Family::INET)

For this case, compiler should match the first match the first overload, and tell me, expected argument #2 to 'TCPServer.new' to be Int, not Int | Nil, right?

1 Like

I think it’s a compiler issue? any idea?

I would say this is compiler error, and this overloading is causing the confusion.

Reduced:

record Test, a : String?, b : Int32?

class Foo
  def initialize(a : String, b : Int32)
  end
  def initialize(b : Int32, dummy = true)
  end
end

test = Test.new("foo", 1234)
Foo.new(test.a, test.b) # => Error: expected argument #1 to 'Foo.new' to be Int32, not (String | Nil)

Yes, this is a bug introduced since 1.5.1.

record Test, a : String?, b : Int32?

class Foo
  def initialize(a : String, b : Int32)
  end
  def initialize(b : Int32, dummy = true)
  end
end

test = Test.new("foo", 1234)
Foo.new(test.a, test.b)

Same reduced code get a not so misleaded error message in 1.5.1

In 1.cr:12:5

 12 | Foo.new(test.a, test.b) # => Error: expected argument #1 to 'Foo.new' to be Int32, not (String | Nil)
          ^--
Error: no overload matches 'Foo.new' with types (String | Nil), (Int32 | Nil)

Overloads are:
 - Foo.new(a : String, b : Int32)
 - Foo.new(b : Int32, dummy = true)
Couldn't find overloads for these types:
 - Foo.new(Nil, Int32)
 - Foo.new(Nil, Nil)
 - Foo.new(String, Nil)

I will create a issue for this.

@asterite Misleading error message when type is not match · Issue #12720 · crystal-lang/crystal · GitHub

@zw963 Please don’t “at” me. Thank you! I’m not working on Crystal anymore. Only when I reaaaly feel like doing it. The project is now in charge of others.

6 Likes

I’m not working on Crystal anymore. Only when I reaaaly feel like doing it.

I consider this is a reaaaaly bad news.

Why? It’s not like I’ve been helping a lot lately. And the language keeps moving forward.

Another way to explain myself: don’t “at” me because I’m not the one that will fix these bugs. I might fix some bugs if I have time and if I find something interesting. But by "at"ing me it might look to others that they should do the same, because I’m the one they should ping. But that’s not true.

5 Likes

I think in general you should only mention somebody if you know they’re interested in that message or answer to them directly. Inflationary use is not good.
Arbitrarily pinging people can be considered rude, regardless of their commitment to fixing bugs.

There is no connection to asterite in this comment.

3 Likes

I think if someone finds and confirms a bug, just submit issues directly may be sufficient, this might be more appropriate than “at” someone in the forum.

1 Like

Regarding the error message, it’s not clear to me that it’s a bug.

You are passing String and Int32 | Nil. There are two overloads: one with String and Int32, and another one with Int32 and any type. So we can produce two errors:

  • for the second argument, say that you passed Int32 | Nil but only Int32 is accepted (because the first argument matches just fine)
  • for the first argument, say that you passed String but only Int32 is accepted (because the second argument matches just fine)

In my mind given multiple overloads there’s no clear winner. The compiler just gives a suggestion, and then lists all overloads. You can always take a look at all the overloads to see where you made a mistake.

But, if you can come up with an heuristic of how to improve this, that’s more than welcome.

But, if you can come up with an heuristic of how to improve this, that’s more than welcome.

Following is a lightly modified version (add a not_nil! to test.a)

record Test, a : String?, b : Int32?

class Foo
  def initialize(a : String, b : Int32)
  end
  def initialize(b : Int32, dummy = true)
  end
end

test = Test.new("foo", 1234)
Foo.new(test.a.not_nil, test.b)

I get following error message:

Showing last frame. Use --error-trace for full trace.

In 1.cr:12:16

 12 | Foo.new(test.a.not_nil!, test.b)
                     ^-------
Error: expected argument #1 to 'Foo.new' to be Int32, not String

Overloads are:
 - Foo.new(a : String, b : Int32)
 - Foo.new(b : Int32, dummy = true)

The argument #1 is a String exactly, totally same as the first overload Foo.new(a: String, ...), why compile recommended the secondary overload? should we recommended on the first one?

I’m pretty sure its because the second parameter of the second overload doesn’t have have a type restriction. So it technically would allow anything while the first overload is out of the running since b must be Int32 and test.b is Int32?.

Well, your guess seems to be correct, but, i consider that not make sense. if the first overload expect the the argument #1 to be a String, i pass a String parameter too, from the user perspective, following error more meaningful?

 12 | Foo.new(test.a.not_nil!, test.b)
                     ^-------
Error: expected argument #2 to 'Foo.new' to be Int32, not (Int32 | Nil)

Overloads are:
 - Foo.new(a : String, b : Int32)
 - Foo.new(b : Int32, dummy = true)

What if you actually intended to pass an int as the first argument? How can the compiler know which overload you wanted?

What if you actually intended to pass an int as the first argument? How can the compiler know which overload you wanted?

Why would the compiler think so? the user gave the exact argument #1 type, it match the first overload argument #1 exactly, so the compiler know user prefer the first overload. :grin:

what you think?


EDIT: as the user, me, i do the exactly things as above, i try to give the argument #1 the correct type, even, remove the Nil use .not_nil!, but the compiler led me completely in another direction that was confusing me.

So we have the def types

  • String, Int32
  • Int32, _

And the argument types are String, Int32?.

The options for changing the types to match are:

  • change #1 from String to Int32
  • change #2 from Int32? to Int32

Both can be valid suggestions and they involve the same amount of changes.

I think the suggestion to drop the Nil type from Int32? to Int32 is more likely to catch the intention. Not accounting for a value’s nilability is a common cause of error. Chances are high that that’s exactly the suggestion the user needs.

In general terms, restricting a union to a subset of its members is a smaller change and more likely to be helpful than changing one type into another unrelated type.
So I think the compiler error message could take this into consideration.

I think so too. The problem is that implementing the that will prove to be quite tricky. Good luck!

From my experience, yes