Problem when trying to rewrite a modern const-concat

const-concat is a crate to solve the problem of concating const strs. The crate was created 5 years ago when const features was unstable. I tried to rewrite the crate with today's stable features, but the compiler says "temporary value dropped while borrowed", while the old code (only removed the feature flags that were unstable at that time) that seems to be equivalent works fine.

use core::mem::ManuallyDrop;

#[repr(C)]
struct Both<const LA: usize, const LB: usize>([u8; LA], [u8; LB]);

#[repr(C)]
union Concat<const LA: usize, const LB: usize, const LS: usize> {
    from: ManuallyDrop<Both<LA, LB>>,
    to: ManuallyDrop<[u8; LS]>,
}

pub const unsafe fn concat<const LA: usize, const LB: usize, const LS: usize>(a: [u8; LA], b: [u8; LB]) -> [u8; LS] {
    // const_assert_eq!(LA + LB, LS);
    ManuallyDrop::into_inner(Concat { from: ManuallyDrop::new(Both(a, b)) }.to)
}

#[macro_export]
macro_rules! const_concat {
    () => {
        ""
    };
    ($a:expr) => {
        $a
    };
    ($a:expr, $b:expr) => {{
        let bytes: &'static [u8] = unsafe {
            &$crate::concat::<
                { $a.len() },
                { $b.len() },
                { $a.len() + $b.len() },
            >(
                *$a.as_ptr().cast::<[u8; { $a.len() }]>(),
                *$b.as_ptr().cast::<[u8; { $b.len() }]>(),
            )
        };

        unsafe { core::mem::transmute::<_, &'static str>(bytes) }
    }};
    ($a:expr, $($rest:expr),*) => {{
        const TAIL: &str = const_concat!($($rest),*);
        const_concat!($a, TAIL)
    }};
    ($a:expr, $($rest:expr),*,) => {
        const_concat!($a, $($rest),*)
    };
}

playground

All you need to do is replace let bytes with const BYTES.

1 Like

final version: https://github.com/Berylsoft/foundations/blob/0e5b187/const_concat.rs

Note that expanding the $a and $b inside an unsafe block allows the caller to do unsafe operations in them without explicitly writing unsafe. I'm not sure how exploitable this is within a const, but in other contexts it's pretty problematic.

1 Like

For UB in const evaluation, the compiler reserves the right to produce arbitrarily useless compile errors, to emit arbitrarily harmful executable code, and to arbitrarily change what the resulting behavior is, even within a single compilation session. It does ensure that the act of compiling the code will not do anything outside the normal domain of what the compiler could do[1].

In practice, the compiler, on a best-effort basis, behaves reasonably. Operations which could never be valid cause compilation errors, and the worst possible outcome is that you get an arbitrary byte-valid instance of the produced type (but it could very well violate safety requirements), allowing the rest of compilation to proceed unaffected.

(This should all be stated as part of the RFC which permitted unsafe in const contexts.)

A partial motivating factor for the hedged position is the existence of "library UB;" while for users there's not much practical difference between violating language validity and violating std safety (both expose you to fully arbitrary misbehavior), one notable practical difference is that the constant evaluator (Miri with validation turned down) can only diagnose "language UB." To the language, "library UB" doesn't exist and is just using the implementation as it exists.

(A primary motivator is the scope of UB in Rust is significantly larger than other languages like, say, C++. C++ constexpr evaluation is specified to error when encountering any UB. The set of UB which the Rust constant evaluator catches in practice includes all of the UB that C++ would catch.)


  1. However, note that what the compiler can do includes running arbitrary code via buildscripts and procedural macros. You can bound rustc to only the (infinite) set of calls it can make to the proc macros available to a given compilation session, but once cargo is involved, most bets are off. ↩ī¸Ž