Typestate const woes

In my crate weight_matchers, I managed to build a specialised binary tree in const Rust. As I explained in the blog, the constraints of const forced me to move part of the code out of the impl, into a macro.

This left open a back door into the data. I should close that after the constructor macro finished. To this end I’m experimenting with a simple typestate, a bool that distinguishes when I’m done with building. I move all my construction support stuff into this impl, which is fine. Only the final method barks.

// Default BUILT to the desired final state, so callers don’t need to bother with it.
pub struct Weights<W, V, const LEN: usize, const BUILT: bool = true> {
    nodes: [(W, W, V); LEN],
}

impl<W: PartialOrd + Default, V, const LEN: usize> Weights<W, V, LEN, false> {
    pub const fn build(self) -> Weights<W, V, LEN> {
        Weights::<W, V, LEN> { nodes: self.nodes }
    }
}

So, I’m removing the zero cost newtype wrapper, and replacing it with essentially the same (but for the value of the const.) Yet the compiler complains. I wonder what drop even means in this context. Even if I had an impl Drop, it should get optimised away, when I’m reusing the whole content?

error[E0493]: destructor of `Weights<W, V, LEN, false>` cannot be evaluated at compile-time
   --> src/lib.rs:179:24
    |
179 |     pub const fn build(self) -> Weights<W, V, LEN> {
    |                        ^^^^ the destructor for this type cannot be evaluated in constant functions
180 |         Weights::<W, V, LEN> { nodes: self.nodes }
181 |     }
    |     - value is dropped here

I tried a different aproach (even though unsafe seems overblown for my purpose.)

    pub const fn build2(self) -> Weights<W, V, LEN> {
        unsafe { std::mem::transmute(self) }
    }

All else being equal, surely the two incarnations Weights<_, _, _, false> and Weights<_, _, _> must have the same size. But the compiler is getting muddled about that.

error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
   --> src/lib.rs:184:18
    |
184 |         unsafe { std::mem::transmute(self) }
    |                  ^^^^^^^^^^^^^^^^^^^
    |
    = note: source type: `Weights<W, V, LEN, false>` (size can vary because of [(W, W, V); LEN])
    = note: target type: `Weights<W, V, LEN>` (size can vary because of [(W, W, V); LEN])

I'm not sure how to work around this, but can point you to some issues.

See this comment and that issue more generally.

This issue, or perhaps this one. Though incidentally, layout could differ between the two if you don't put repr(transparent) on your struct.

1 Like

Maybe like this?

impl<W: PartialOrd + Default, V, const LEN: usize> Weights<W, V, LEN, false> {
    pub const fn build(self) -> Weights<W, V, LEN> {
        let this = ManuallyDrop::new(self);
        // ptr still valid after cast: according to the docs
        // "ManuallyDrop<T> is guaranteed to have the same layout as T"
        let ptr = (&raw const this).cast::<Self>();
        // using `ptr::read` on the field `nodes` is sound as a logical move here:
        // this `nodes` field (even the whole value `this` that contains it)
        // isn't accessed anymore afterwards, and no drop-glue runs either
        Weights::<W, V, LEN> { nodes: unsafe { (&raw const (*ptr).nodes).read() } }
    }
}
2 Likes

Huh, I attempted something like this when first replying:

impl<W: PartialOrd + Default, V, const LEN: usize> Weights<W, V, LEN, false> {
    pub const fn build(self) -> Weights<W, V, LEN> {
        let this = ManuallyDrop::new(self);
        Weights::<W, V, LEN> { nodes: unsafe { std::mem::transmute_copy(&this) } }
    }
}

But that works while my original got the destructor warning. Wonder what I missed...

Thank you for the very well founded background information! It’s sad to find these dark corners of Rust. At the same time, it’s great that they’ve already been identified and have a chance of being solved some day!

Since they talk badly about transmute_copy, I went with @steffahn’s solution. I guess that doen’t require #[repr(transparent)]?

They ultimately do the same thing in this case. transmute_copy has more checks that won't apply to this case, so you might as well skip it. (The source code is simple.)

You don't need the repr(transparent) if you never transmute Weights or read a pointer to Weights as if it were a pointer to the array, etc.

Is there technically any guarantee that the layout doesn't change with the new const BUILT: bool -generic parameter?

I don't believe so, which is why I said the repr was required in the context of the OP. But the last two code snippets just deal with the array and create a new Weights (with a different const bool parameter) outside of unsafe.

(OTOH I can't imagine the repr doing any harm, but they did ask if they really needed it...)