`concat!` can and should support concatting `const`

It's now possible to write a fairly simple and fully stable const_concat!: [playground]

#![no_std]

use core::mem::ManuallyDrop;

const unsafe fn transmute_prefix<From, To>(from: From) -> To {
    union Transmute<From, To> {
        from: ManuallyDrop<From>,
        to: ManuallyDrop<To>,
    }

    ManuallyDrop::into_inner(
        Transmute {
            from: ManuallyDrop::new(from),
        }
        .to,
    )
}

/// # Safety
///
/// `Len1 + Len2 >= Len3`
#[doc(hidden)]
#[allow(non_upper_case_globals)]
pub const unsafe fn concat<const Len1: usize, const Len2: usize, const Len3: usize>(
    arr1: [u8; Len1],
    arr2: [u8; Len2],
) -> [u8; Len3] {
    #[repr(C)]
    struct Concat<A, B>(A, B);
    transmute_prefix(Concat(arr1, arr2))
}

#[macro_export]
macro_rules! const_concat {
    () => ("");
    ($a:expr) => ($a);

    ($a:expr, $b:expr $(,)?) => {{
        const BYTES: [u8; { $a.len() + $b.len() }] = unsafe {
            $crate::concat::<
                { $a.len() },
                { $b.len() },
                { $a.len() + $b.len() }
            >(
                *$a.as_ptr().cast(),
                *$b.as_ptr().cast(),
            )
        };
        unsafe { ::core::str::from_utf8_unchecked(&BYTES) }
    }};
    
    ($a:expr, $b:expr, $($rest:expr),+ $(,)?) => {{
        const TAIL: &str = $crate::const_concat!($b, $($rest),+);
        $crate::const_concat!($a, TAIL)
    }}
}

#[test]
fn tests() {
    const SALUTATION: &str = "Hello";
    const TARGET: &str = "world";
    const GREETING: &str = const_concat!(SALUTATION, ", ", TARGET, "!");
    const GREETING_TRAILING_COMMA: &str = const_concat!(SALUTATION, ", ", TARGET, "!",);

    assert_eq!(GREETING, "Hello, world!");
    assert_eq!(GREETING_TRAILING_COMMA, "Hello, world!");
}

Rust std would use #[allow_internal_unstable] or just inline the function definition rather than #[doc(hidden)] and prayers. Obviously it'd be better to write fn concat [...] -> [u8; Len1 + Len2] rather than having that as a safety invariant, but I want this to be stabilizable with today's features.

A version of this that supports the other types concat! accepts and stringify!s is available on crates-io as const_format::concatcp!.

5 Likes

Note that your macro is unsound: it supports creating non-UTF8 strings:

println!("{:?}", const_concat!(b"\xFF", b"\xFF"));

The fix is to ensure &str is provided:

($a:expr, $b:expr $(,)?) => {{
    const A: &str = $a;
    const B: &str = $b;
    const BYTES: [u8; { A.len() + B.len() }] = unsafe {
        $crate::concat::<
            { A.len() },
            { B.len() },
            { A.len() + B.len() }
        >(
            *A.as_ptr().cast(),
            *B.as_ptr().cast(),
        )
    };
    unsafe { ::core::str::from_utf8_unchecked(&BYTES) }
}};
3 Likes

Currently concat!() expands to string literal and it's common for other macros to read its content like include!(concat!(env!("OUT_DIR"), "/out.rs')). AFAIK content of the const values are not accessible during the macro expansion stage. Technically it's possible to implement it without breaking change by make it keep returning string literal if it doesn't need any const value. But it would hurt learnability to make it sometimes return string literal but sometimes not.

I think it's nice to add it as another macro with separate name.

5 Likes