A macro to assert that a type *does not* implement trait bounds

Some or many may know the trick for the opposite assertion, that a type does in fact implementation a trait, which comes in handy when one wants to test for Send and Sync at an API boundary.

fn check_if_send<T: Send>() { }
// Yes, we can send `usize` to other threads
check_if_send::<usize>();

But that does not protect from accidentally enabling more auto-traits than intended. So it is possible to do the opposite check? Can we fail to compiler because a trait is implemented? Yes we can, even with a somewhat handy macro!

Usage

// compiles:
assert_not_impl!(u8, From<u16>);
// fails to compile:
// assert_not_impl!(u8, From<u8>);

Implementation:

macro_rules! assert_not_impl {
    ($x:ty, $($t:path),+ $(,)*) => {
        const _: fn() -> () = || {
            struct Check<T: ?Sized>(T);
            trait AmbiguousIfImpl<A> { fn some_item() { } }

            impl<T: ?Sized> AmbiguousIfImpl<()> for Check<T> { }
            impl<T: ?Sized $(+ $t)*> AmbiguousIfImpl<u8> for Check<T> { }

            <Check::<$x> as AmbiguousIfImpl<_>>::some_item()
        };
    };
}

play

Explanation

The type inference of Rust can infer the type parameter of a trait. When there is only one applicable impl for a specific type then it infers that the type parameter must be the same as the one from that impl. This implies that more available impls can lead to worse type inference. We then construct exactly that situation: One trait impl is applicable for all types (AmbiguousIfImpl<()>) while the other is applicable only if the type implements the queried trait bounds (AmbiguousIfImpl<u8>), so that the traits type parameter can not be inferred when the second impl is also available. We then require the type inference by calling a method on the impl of the trait without specifying the traits type parameter.

12 Likes

Wow, that's a really clever trick! You should make a PR to the static-assertions crate.

It's a bit annoying that the assertion won't be triggered when using the T as a type parameter to a generic function though, but I can see why that would be the case.

fn main() {    
    // Fails to compile, Cell is Send
    // assert_not_impl!(Cell<u8>, Send);

    // equivalent of the previous line
    do_something::<Cell<u8>>();
}

fn do_something<T>() {
    assert_not_impl!(T, Send);
}
2 Likes

Yep, I've started a PR to static-assertions.

The case of type parameter exemplifies an important part of type reasoning in Rust. A generic type parameter behaves like an introduced new type in a universe where the available impls are artificially restricted to exactly the required trait bounds. This guarantees that do_something can not change its behaviour based on facts that have not been mentioned in its type interface. Since T does not assert Send, its usage in the macro behaves as if it were never available.

2 Likes

For the sake of completeness, we have to consider

assert_not_impl!([T : Copy], Cell<T>, Send);

However, such assertion passing only means that there exists at least one T / it could be possible for some T that Cell<T> do not impl Send. So indeed a macro supporting generics would be more misleading than helpful, imho.

Slightly confused what you mean with assert_not_impl!([T: Copy], ..) as that is not accepted by the macro and I assume you mean a new type parameter T. (But how is Copy relevant?).

fn foobar<T: Copy>() {
    assert_not_impl!(Cell<T>, Send);
}

However, your interpretation of what it means is not entirely correct. It means: in this context, there is no usable impl of Send for Cell<T>. Not more, not less. If T is a concrete type then of course that asserts it for all scopes since universes have to be consistent. If it is a type parameter it asserts in exactly the same manner as type inference is later able to utilize that information.

It is not asserted after monomorphization , I see how that could be confusing (and I should add it to the PR documentation). If one were to base some unsafe code on these assertions I would expect that level of understanding from the user, though. For example, to assert that a custom Box<T> is only Send when T is does not require any assertions after monomorphization but is covered by:

fn not_send_without_t<T>() {
    assert_not_impl!(Box<T>, Send);
}
fn send_with_t<T: Send>() {
    assert_impl!(Box<T>, Send);
}

My bad, I did not expect your macro to work with generic types from scope altogether, hence my imagining a [generic_params_and_bounds], optional first parameter to your macro. After reading again what the macro expands to, I see how it can work with "contextual generic types".

Million dollar question: What does the compiler error message look like?

Far from perfect but not too bad, that is with the trait's name already being customized to hint at the error. The explanation and links that rustc hints at are obviously bogus.

   Compiling playground v0.0.1 (/playground)
error[E0282]: type annotations needed
  --> src/main.rs:10:45
   |
10 |             <Check::<$x> as AmbiguousIfImpl<_>>::some_item()
   |                                             ^ cannot infer type
...
23 |     assert_not_impl!(Cell<u8>, Send);
   |     --------------------------------- in this macro invocation

error: aborting due to previous error

For more information about this error, try `rustc --explain E0282`.
error: Could not compile `playground`.

To learn more, run the command again with --verbose.

I did not put too much thought into improving it so there may be obvious ways to clarify the assertion failure itself. Feel free to suggest ideas.

That's fair enough, with the macro being highlighted in the error, it should be kind of obvious. Thanks for sharing this.

1 Like

And when the macro comes from an external crate the message is even more concise:

error[E0282]: type annotations needed
 --> src/main.rs:5:2
  |
5 |     assert_not_impl!(u8, Copy);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type
  |
  = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.