Type inference bug?

Hi,

I'm trying out some code with const generics and I'm running into some issues with type inference. Is there any way to make this work without having to specify let _: Foo<4, 0> = ...;?
Playground

struct Foo<const N: usize, const M: usize = 0> {
    _x: [usize; N],
    y: Option<[usize; M]>,
}

impl<const N: usize, const M: usize> Foo<N, M> {
    fn new(x: [usize; N]) -> Self {
        Self {
            _x: x,
            y: None
        }
    }
    
    fn new_without_y(x: [usize; N]) -> Foo<N, 0> {
        Foo {
            _x: x,
            y: None
        }
    }
    
    fn with_y(mut self, y: [usize; M]) -> Self {
        self.y = Some(y);
        self
    }
}

fn main() {
    // This doesn't compile: `Foo<4, 0>` should be inferred
    // https://github.com/rust-lang/rust/issues/98931
    // let _ = Foo::new([0; 4]);
    
    // This compiles: `Foo<4, 8>` is inferred
    let _ = Foo::new([0; 4]).with_y([0; 8]);
    
    // This doesn't compile:
    // Even though the return type is Foo<N, 0>, the compiler says it can't infer M
    // Not sure if known issue, it seems different from the one related to default const generics
    let _: Foo = Foo::new_without_y([0; 4]);
}

Here's the reason it doesn't work. The fully-qualified version of your let _: Foo = Foo::new_without_y([0; 4]); is:

let _: Foo<4, 0> = <Foo<4, M>>::new_without_y([0; 4]);

where M is any integer at all.

This is because your new_without_y method is defined inside an impl block that implements the method for all Foo<N, M> types, regardless of whether M is zero or not.

The fix is to separate new_without_y into a different impl block, that only applies to when M is zero:

impl<const N: usize> Foo<N, 0> {
    fn new_without_y(x: [usize; N]) -> Foo<N, 0> {
        Foo {
            _x: x,
            y: None
        }
    }
}
1 Like

this is expected, although not very useful IMO, behavior. the problem is, default generic arguments are not used during inference. some relative readings:

the first error can be worked around by implementing new() for a specialized type:

// Foo<N> is short for Foo<N, 0>
impl<const N: usize> Foo<N> {
    fn new(x: [usize; N]) -> Self {
        Self { _x: x, y: None }
    }
}

let _ = Foo::new([0; 4]);

but this modification will break the second case, since with_y() returns the same type as Self. to work around this, you can also modify with_y() so that it changes the return type:

impl<const N: usize, const M: usize> Foo<N, M> {
    // changes the return type based on `y`
    fn with_y<const M2: usize>(mut self, y: [usize; M2]) -> Foo<N, M2> {
        Foo {
            _x: self._x,
            y: Some(y),
        }
    }
}

let _ = Foo::new([0; 4]).with_y([0; 8]);

for the third case, you can use the same technique as the first one, but there's another issue for the annotation: you cannot use _ as placeholder for const generics, so you either need concrete values for const generics in the ascribed type, or you don't use type ascription and rely on type inference only.

3 Likes

This seems to work! Cool trick :slight_smile: Thanks for the help!

This also works :slight_smile:

This feature is currently being stabilized! Stabilize `feature(generic_arg_infer)` by BoxyUwU · Pull Request #141610 · rust-lang/rust · GitHub