Poor man's specializaton in Rust

While working on our compiler and runtime I have wrote a decent number of macroses to help developers to write new kernels for the system. When the developers started to use them, it quickly became obvious that a custom error messages would help a lot. Some of those macroses were "paired". If a user invoked one, they should also invoke another. And if they forgot, they were getting a fairly nice and verbose error from the Rust compiler. It was also utterly unhelpful if the developer was not familiar with the internals of the macroses I wrote.

I have reached out to Yandros on #macros channel in the Rust's Discord server. The following solution should be credited in its entirety to this wonderful person.

The idea is simple. There is rustc_on_unimplemented derive macro inside Rust's stdlib. It defines a custom message to be returned when a trait implementation is missing. For example, this is how function traits report helpful errors like "wrap the {Self} in a closure with no arguments: || {{ /* code */ }}". This would be perfect, but it is not allowed for usage outside of the stdlib.

Yandros came up with this solution. Let me explain how it works a bit.

The two macroses I want to be paired are first! and second!. I want to have a nice suggestion in the error output if the user makes a mistake like this:

first!(A for u64);
second!(A for u16); // Note the intentional wrong type (a mistake)

We define a "poor man's specialization" wrapper and implement it with a const boolean set to true when the "second!" trait implementation exists. When it does not exist, we also have a Fallback implemetnation. The difference is that in one case the boolean is associated with the Wrapper struct, while in another it is associated with a trait that is also implemented for the Wrapper.

The next trick is to force the compiler to make the check at the compile time. Yandros did it by using the constant as a slice's usize on a field. Rust compiler is very careful about knowing the sizes of every slice at compile time and is not skipping out on the check:

const __EAGER: ::core::primitive::bool = {
    if ! __poormans_specialization::Wrapper::<$T>::__DOES_IMPL {
        ::core::panic!(::core::concat!("\n   = note: ", $message));
    }
    const _: [(); __EAGER as ::core::primitive::usize] = [];
    false
};

This results in all the regular errors from the Rust compiler (I did not want to suppress them) and a new one:

error[E0080]: evaluation of constant value failed
  --> src/main.rs:73:1
   |
73 | first!(A for u64);
   | ^^^^^^^^^^^^^^^^^ the evaluated program panicked at '
   = note: you may want to call `second!(A for u64)`', src/main.rs:73:1

A slightly different approach allows us to error out before the trait errors. The eager code is slightly different:

const __EAGER: ::core::primitive::bool = {
    if ! __poormans_specialization::Wrapper::<$T>::__DOES_IMPL {
        ::core::panic!(::core::concat!("\n   = note: ", $message));
    }
    {
        struct S<const __: ::core::primitive::bool>;
        impl ::core::marker::Unpin for S<__EAGER> {}
    }
    false
};

I must confess that I do not understand the reason one approach suppresses the errors and the other is not.

If you want to use the tricks defined there, I think you should read the full playground code. You may also want to consider autoref based specialization, as described in here. There is even a Cargo crate spez to make the latter pattern easier to use.

I ended up not using this trick in my production code. I tried it out with our developers and we have concluded that the loss of readability of the code outweights the benefits of better error messages. But I learned a lot about Rusts by trying it out. The most important bit I learned is how awesome and smart Rust community is.

Posted On

Category:

Tags: /