Slicing strings in const

How can I get this to compile?

const fn drop_last_ascii_char(s: &str) -> &str {
    let len = s.len();
    &s[..len-1]
}
error[E0015]: cannot call non-const operator in constant functions
 --> src/lib.rs:3:7
  |
3 |     &s[..len-1]
  |       ^^^^^^^^^
  |
  = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants

I know it can be done with unsafe, but is it possible to do this safely?

use core::{str, slice};

const fn drop_last_ascii_char(s: &str) -> &str {
    let len = s.len();

    if s.as_bytes()[len - 1] >= 128 {
        panic!("can't slice non-ascii char");
    }

    unsafe {
        str::from_utf8_unchecked(
            slice::from_raw_parts(s.as_ptr(), len - 1)
        )
    }
}

Not very satisfying, but:

        const fn drop_last_ascii_char(s: &str) -> &str {
            let len = s.len();
            let s = s.as_bytes().split_at(len - 1).0;
            match core::str::from_utf8(s) {
                Ok(s) => s,
                Err(_) => panic!(),
            }
        }

Or use this instead of the match (along with your ascii check) to avoid the utf8 check and at least make the unsafe part simpler.

            unsafe { core::str::from_utf8_unchecked(s) }
4 Likes

str::split_at and friends should really be const. You could reimplement a const version and then use it.

Agreed.

If anyone wants a fairly-simple PR, if you submit one making them unstably const I'll merge it, assuming of course that the code change to do that is fairly straight-forward (not needing a bunch of reimplementing or weird internal features).

Then it would probably stabilize fairly quickly, but that would be a separate step.

EDIT: hmm, looks like it'll depend in is_char_boundary being const fn, so maybe start with that.

1 Like

Even most of just regular slice manipulation is also nonconst currently. This is most likely due to that panicking with formatted message is nonconst, so making any panicking indexing operation const will require const_eval_selecting between a const panic or the current more informative panic message, and stop using the more convenient assert!s.

(Regular slices have the out of using slice patterns at least some of the time.)

But this pattern already exists and should likely be acceptable to use more widely, especially for things as widely useful (and possible to do via workaround) as slice manipulation.

4 Likes

You can also match on byte slices. I like this solution, because it clearly shows all edge-cases (for example passing empty slice).

const fn drop_last_ascii_char(s: &str) -> &str {
    match s.as_bytes() {
        b"" => todo!(), // Eiter panic, or return an empty slice
        [init @ .., last] if *last < 128 => {
            match std::str::from_utf8(init) {
                Ok(s) => s,
                Err(_) => unreachable!(),
            }
        }
        _ => panic!("can't slice non-ascii char"),
    }
}
1 Like

Good point. A couple things like https://doc.rust-lang.org/nightly/std/primitive.str.html#method.split_at_checked can avoid that, though. So those parts would hopefully be easy.