Hiroto Kagotani
Hiroto Kagotani

Reputation: 400

Why does method-call on ambiguous numeric cause error?

In the reference at Literal expressions, I see

If the token has no suffix, the expression's type is determined by type inference:

  • ...
  • If the program context under-constrains the type, it defaults to the signed 32-bit integer i32.
  • ...

But I get an error from the following code:

let i = (-100).abs();
error[E0689]: can't call method `abs` on ambiguous numeric type `{integer}`

Why does method-call on ambiguous numeric cause error? Is not the type of '-100' inferred as i32?

Upvotes: 3

Views: 4229

Answers (1)

E_net4
E_net4

Reputation: 30013

Let us first quote all of the relevant part on that page (emphasis mine):

  • If an integer type can be uniquely determined from the surrounding program context, the expression has that type.
  • If the program context under-constrains the type, it defaults to the signed 32-bit integer i32.
  • If the program context over-constrains the type, it is considered a static type error.

Now, this description may be misleading due to how the verb "constrain" was employed. One is usually familiar with generic types constrained by traits, but the reference is possibly not to be interpreted in that way. Types being under-constrained and over-constrained refer to cases in which additional requirements were imposed over the type which must be considered by type inference, which happens when calling a method of that type.

So no, it does not apply to any kind of constraint over a type. Here is a counter-example.

fn is_signed(x: impl num_traits::Signed) {
    let _ = x.abs(); // can call abs thanks to Signed trait
}

fn main() {
    is_signed(-5);
}

This code (Playground) may seem to "constrain" -5 to require the number to be signed, and since we have implementations of Signed for multiple integer types, this should be ambiguous. But it does, in fact, compile as of Rust 1.63.0.

One other peculiar example.

trait Foo {}

impl Foo for u8 {}
impl Foo for u16 {}

fn foo(_: impl Foo) {}

fn main() {
    foo(5);
}

This code (Playground) does not compile, but not for the reason that you might expect:

error[E0277]: the trait bound `i32: Foo` is not satisfied
 --> src/main.rs:9:5
  |
9 |     foo(5);
  |     ^^^ the trait `Foo` is not implemented for `i32`
  |
  = help: the following other types implement trait `Foo`:
            u16
            u8
note: required by a bound in `foo`
 --> src/main.rs:6:16
  |
6 | fn foo(_: impl Foo) {}
  |                ^^^ required by this bound in `foo`

The integer's type inference mechanism allegedly went for the default type i32, even though the only possible implementations are for u8 and u16. It would be another ambiguous situation regardless.

However, things are clearly different when one attempts to call a method directly associated to a type, as done in (-100).abs(). From the moment you called the method abs, which even exists for multiple integer types, the compiler saw the integer as having an over-constrained type, and aligned with that reasoning, a static type error occurred. The compiler provides a different explanation for this, which is embodied in error E0689.

A method was called on an ambiguous numeric type.

In any case, the real reason why that code does not compile is because the compiler could not identify a type for the integer in that particular case, and did not default the integer type to i32. The Rust reference is not a standard reference for the language, and may be either incomplete or contain outdated information. Other than the misleading concept of over-constraining and under-constraining, the description in the reference matches the compiler's behavior at the time of writing. At this point, we may well be dealing with just another compiler nuance.

See also:

Upvotes: 6

Related Questions