Why is the size of an array slice not known at compilation time?

I was doing rustlings exercise 04_primitive_types/primitive_types4.rs, and my initial solution was:

let a = [1, 2, 3, 4, 5];

// TODO: Get a slice called `nice_slice` out of the array `a` so that the test passes.
// Note to self: Not sure why it cannot know a[1..4] is of type [i32; 3] :-/
let nice_slice = a[1..4];

However, this leads to the following compilation error:

error[E0277]: the size for values of type `[{integer}]` cannot be known at compilation time
 --> primitive_types4.rs:6:9
  |
6 |     let nice_slice = a[1..4];
  |         ^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `[{integer}]`
  = note: all local variables must have a statically known size
  = help: unsized locals are gated as an unstable feature
help: consider borrowing here
  |
6 |     let nice_slice = &a[1..4];
  |                      +

Sure enough, borrowing fixes the compilation error but...

How come the compiler can't know the size?

If I read a[1..4] I would think "Yep, nice_slice is definitely of type [i32; 3]".

But why doesn't the compiler reach this conclusion?


also I am almost 100% certain this has been asked before but I could not fine it on this forum. the closest thing is the following post:

but that thread still does not explain me why the size cannot be inferred.

1 Like

Perhaps this could be done through several changes to the language, but the way arrays, indexing, and ranges work currently don't allow this.

To start, the length of arrays is part of the type, which means it can't be derived from values or other runtime information. Ranges are only generic over the numeric type, so they can't convey any type information about their length. The indexing only has access to the range once it's fully constructed, so it has no way of seeing the length either. And the output type of indexing is only dependent on the input types, so it also doesn't have any way of knowing the length.

These things are sort of easy for a human but really difficult to implement in a type system in a general manner, outside some special cases. The type of an expression cannot in general depend on whether the expression is compile-time evaluatable or not. So a[1..2] cannot have a different type than a[i..j]. Supporting such things would require a compile-time evaluation system much more advanced than what Rust can do right now, especially if foo itself is a run-time variable rather than a constant.

It would be possible to have a different range type that is generic over length; this compiles:

use std::ops::Index;

struct ArrayRange<const LEN: usize>(usize);

impl<T, const ALEN: usize, const RLEN: usize> Index<ArrayRange<RLEN>> for [T; ALEN] {
    type Output = [T; RLEN];
    fn index(&self, ArrayRange(start): ArrayRange<RLEN>) -> &[T; RLEN] {
        (&self[start..][..RLEN]).try_into().unwrap()
    }
}

fn main() {
    let a = [1, 2, 3, 4, 5];
    let nice_slice = a[ArrayRange::<3>(1)];
}
2 Likes

Easy enough when the range is a constant, but what about when it isn't? What single type can occupy position T in

fn slice_off(arr: [i32; 8], end: usize) -> T {
  arr[0..end]
}

?

I think there was a number of proposals for "const indexing". It probably would need a dedicated syntax (e.g. it could look like &a[const { 1..4 }]) and new const indexing traits. Personally, I would love to see it implemented, but for now we can use the TryInto workaround: let arr: &[u32; 3] = &a[1..4].try_into().unwrap();.

You can consider the previous replies in the context of strict static typing. Note first that the length of an array is part of its type (arrays of different lengths are different types, even if the type of the values within are the same). But the length of slices (like [i32]) are not part of their type, which is why they are not Sized -- they don't have a fixed length (or size).

Considering your code:

//               vvvvvvv call this expr
let nice_slice = a[1..4];
//                 ^^^^ call this idx

What's the type of expr? It's a <[i32; 5] as Index<_>>::Output, where the _ is the type of the value passed within the square brackets -- the type of idx. And what's the type of idx? It's a Range<usize>.

So even without knowing the exact type of expr, just knowing the type of idx is enough to conclude that the type of expr cannot depend on the length:[1] expr must have the same type for every value of Range<usize>, due to Rust being strictly and statically typed. There can only be one implementation of Index<Range<usize>> for [i32; 5] (and it returns [i32]).

In contrast, the Index implementation that @kpreid provided is generic over the length: ArrayRange<2> and ArrangeRange<3> are different types, so there's a way to get different types as outputs when they're used in indexing.

let b = a[ArrayRange::<3>(1)]; // <[i32; 5] as Index<ArrayRange<3>>>::Output
let c = a[ArrayRange::<2>(1)]; // <[i32; 5] as Index<ArrayRange<2>>>::Output
//           the types can differ due to this generic parameter ^

(So you're actually calling different monomorphized methods for each length, which one hopes gets optimized away.)


Having a[1..4] be an array instead of a slice would require some sort of change to the language (1..4 becomes a type parameterized by the length, indexing by constant range becomes a builtin (language-level) operation, etc).


  1. and thus cannot be an array ↩︎

2 Likes

At a higher level, one reason is that Rust generally would rather that literals and values don't work differently. It would also be awkward if you changed a[i..4] to a[1..4] temporarily for debugging and all of a sudden the code stopped compiling because it was a different type. Or if you try something out with a[1..4], but then try to abstract it to a function using a[i..j] where i and j are parameters (or they're loop indexes, or ...) and it again stops compiling for being a different type.

You can see this in a bunch of places, like how while true { ... } isn't the same as loop { ... }.


As for your specific situation, check out all the slice::*chunk* APIs added in 1.77.

They give you arrays instead of slices, so instead of `&a[1..4]` you might do `a[1..].first_chunk::<3>()` or `a[..4].last_chunk::<3>()`.
2 Likes