`const` blocks in `Drop::drop` is 'always' evaluated, any way to make it conditionally evaluated?

I aim to create a linear type, i.e., a type that is strictly usable for some integer N number of times.

For example, suppose there is a type named Counter<T, EC, MEC> where T is any type and both EC and MEC are type-level integers (they can individually be a const generic as well but easier to code examples using typenum::Unsigned as const-generics-ops is not stabilized) then Counter<T, EC, MEC> can only be dropped only if EC = MEC, or else it should raise a compilation error. Values of type Counter<T, EC, MEC> can exist transiently as long as they are 'forgotten' with core::mem::forget or with a similar mechanism and are eventually consumed returning a value of type Counter<T, EC + 1, MEC>.

The ideal behavior of the program I want is the following:

The following should not be compileable.

use typenum::*;

/** omitted definitions of `Counter`, shown below **/

fn main() {
    let counter: Counter<i32, U4, U5> = Counter::new(70);
}

The following should be compileable.

fn main() {
    let counter: Counter<i32, U4, U5> = Counter::new(70);
    println!("counter EC: {:?}", counter.ec()); // prints 4

    // `counter_one` is Counter<i32, U5, U5>`

    let counter_one = counter.add_one();
    println!("counter_one EC: {:?}", counter_one.ec()); // prints 5

This is because add_one(self) is a consuming method and within it there is a core::mem::forget(self) call to tell Rust not to drop the value of counter.

Here is the full code:

use core::{
    marker::PhantomData,
    mem::{ManuallyDrop, forget},
    ops::{Add, Drop},
};
use typenum::*;

#[repr(transparent)]
struct Counter<T, EC: Unsigned, MEC: Unsigned>(ManuallyDrop<T>, PhantomData<EC>, PhantomData<MEC>);

impl<T, EC, MEC> Counter<T, EC, MEC>
where
    EC: Unsigned,
    MEC: Unsigned,
{
    fn add_one(mut self) -> Counter<T, Sum<EC, U1>, MEC>
    where
        EC: Unsigned + Add<U1>,
        Sum<EC, U1>: Unsigned + Add<U1>,
    {
        let t = unsafe {ManuallyDrop::<T>::take(&mut self.0)};
        const {forget(Self);}
        Counter(ManuallyDrop::new(t), PhantomData, PhantomData)
    }
    
    const fn new(t: T) -> Self {
        Self(ManuallyDrop::new(t), PhantomData, PhantomData)
    }
    
    const fn ec(&self) -> usize {
        EC::USIZE
    }
}

impl<T, EC, MEC> Drop for Counter<T, EC, MEC>
where
    EC: Unsigned,
    MEC: Unsigned,
{
    fn drop(&mut self) {
        println!("Drop is called");
        struct Check<const CEC: usize, const CMEC: usize>;

        const {
            if EC::USIZE != MEC::USIZE {
                panic!("EC is larger than MEC");
            }
        };
    }
}

fn main() {
    let counter: Counter<i32, U4, U5> = Counter::new(70);
    println!("counter EC: {:?}", counter.ec());
    let counter_one = counter.add_one();
    println!("counter_one EC: {:?}", counter_one.ec());
    let counter_two = counter_one.add_one();
    println!("counter_two EC: {:?}", counter_two.ec());
}

[Playground]

This doesn't not compile as it has two compiler errors:

error[E0080]: evaluation of `<Counter<i32, typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B1>, typenum::B0>, typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>> as std::ops::Drop>::drop::{constant#0}` failed
  --> src/main.rs:46:17

error[E0080]: evaluation of `<Counter<i32, typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B0>, typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>> as std::ops::Drop>::drop::{constant#0}` failed
  --> src/main.rs:46:17

This means the panic within the below snippet

const {
    if EC::USIZE != MEC::USIZE {
       panic!("EC is larger than MEC");
    }
};

is triggered for Counter<U4, U5> and Counter<U6,U5>.

Also, what this means is Rust will evaluate the const blocks of the Drop trait implementation for any type that exists within the program.

Question 1

Is there a way to conditionally stop Rust from evaluating the const block within the Drop trait implementation?

Question 2

In the implementation of add_one reproduced below:

fn add_one(mut self) -> Counter<T, Sum<EC, U1>, MEC>
    where
        EC: Unsigned + Add<U1>,
        Sum<EC, U1>: Unsigned + Add<U1>,
    {
        let t = unsafe {ManuallyDrop::<T>::take(&mut self.0)};
        const {forget(Self);}
        Counter(ManuallyDrop::new(t), PhantomData, PhantomData)
    }

The line const {forget(Self);} actually compiles [Playground]. What on Earth is this forget(Self) where Self is the receiver type, not the receiver value!

Not really, and even if it did you'll still have various problems due to potential panics (see for example no-panic; it's not what you're trying to do but you'll suffer from its same problems)

Your Counter type is a tuple struct and that means that Counter/Self is also a function that takes as many arguments as the fields in your struct and returns an instance of it. A function is also a value, and that's what you're forgetting.

1 Like

No, in your example programs, there exist code paths that would also call the destructor, and without those, the const blocks would (AFAICT; see the playground below) not actually execute.

The const { forget(Self); } doesn't really do what you want it to. Instead, it would be forget(self). However that isn't enough to remove the conditional destructor call the compiler inserts for if ManuallyDrop::::take were to panic. Instead, construct ManuallyDrop before that, and it can work: Rust Playground

Now, in this playground, some of the println!("counter EC: {:?}", counter.ec()); calls are also removed. Those, too, unfortunately are places where the compiler does conservatively assume panics can happen. (Well, really, I think println can also actually panic.) And on panic, the "linear type" of yours would be dropped early. Even the call just to counter.ec() is assumed to be something that can panic; or the call to any other function. The linked playground of mine above only works because every function call consumes all of your 'linear' variables currently at play. How usefull all this still is with those limitations, I don't know; but good luck / have fun exploring the possibilities.

1 Like

It can if you process just doesn't have an stdout. Which is perfectly legal if unusual. That's why WriterPanicked even exists.

To summarize:

One
steffahn gave a way to make this 'linear' type works by 'forgetting' self by constructing ManuallyDrop directly instead of using core::mem::forget (core::mem::forget just constructs a ManuallyDrop and drops it immediately). However, it is still possible to trigger the evaluation of the const block within the Drop trait implementation of Counter<T, U3, U5> and Counter<T, U4, U5> even by getting a reference of it [Playground].

Two
The explanation by SkiFire13 on why const { forget(Self); }; compiles is brilliant.

Reproduced below for easy reference:

Your Counter type is a tuple struct and that means that Counter /Self is also a function that takes as many arguments as the fields in your struct and returns an instance of it. A function is also a value, and that's what you're forget ting.

Hi @SkiFire13, I am thinking to remove the triggering of compilation error by using panic!, I might probably need a declarative macro that expands to compile_error!() if true is passed in.

However, I am not sure if const block evaluation would expand all macros therein.

Macros are all expanded before const eval. This also means you can't make a macro conditionally expand to different things depending on the resultof const eval.

1 Like

Hi @bjorn3

Thank you for your response.

I agree, as I had just tried on the Rust playground. Macros cannot be used as a value for deferred evaluation.

I guess this prototype of a linear type has to make do with the fact that it won't work under the no panic! context.

As for the fact that &Counter<U4, U5> triggers the const block evaluation, I am guessing 'autoref specialization' will fix it. Have to implement Drop for &T, ..., &&&...T, lol.

Yeah, and it has to be something that does not trigger a compilation error before const evaluation but triggers a compilation error during / after const evaluation.

Only the special panic! works.

Is that a fact? I thought my experimentation shows it doesn't.

Are you referring to the

println!("counter EC: {:?}", &counter.ec());

lines? That's not about &Counter<U4, U5> at all but merely about the fact that it is considered something that can panic. Here, replace it with some function call that does nothing, and observe the same result!

fn f() {}

fn main() {
    let counter: Counter<i32, U3, U5> = Counter::new(70);

    f(); // this causes code generation for dropping `Counter<i32, U3, U5>`
    //0 + 0; // this would do the same
    //[()][0]; // this would do the same
    // etc..

    let counter_one = counter.add_one();
    let counter_two = counter_one.add_one();
    println!("counter_two EC: {:?}", &counter_two.ec());
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.