Skip to content

RFC: Assume bounds for generic functions #3802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions text/3802-assume-bound.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
- Feature Name: `assume_bound`
- Start Date: 2025-04-21
- RFC PR: [rust-lang/rfcs#3802](https://github.com/rust-lang/rfcs/pull/3802)
- Rust Issue: N/A

# Summary
[summary]: #summary

This feature allows to assume trait bounds on generics so that the caller don't has to proof them pre-monomorph.

# Motivation
[motivation]: #motivation

I am currently writing a lot of generic magic again.
To be more concrete, I am making a framework where lots of functions with generic parameters call eachother.
There, I pass some `impl Key` values, which hold an assoicated type and proof that the generic, which is passed down, implements `Has<T>`, where T is that associated bound.
Now I don't really want to specify that requirement on every function, as that is cumbersome and hard to maintain.
Thus I propose to let me have my unsafe fun to assume trait bounds are fulfilled in the places I am sure about.

Another use case is for complex higher ranked bounds, where a small helper function could be used to hint the compiler that a trait is really implemented.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

When implementing a function with a `where` clause, like this one:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about non-functions

trait Foo<U> where #[unsafe(assume)] U: Iterator {
    type Item where #[unsafe(assume)] <U as Iterator>::Item: Into<u16>;
}

impl Foo<U> for U where #[unsafe(assume)] U: Future {
    type Item = U::Output;
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:0 interesting idea, should probably also work on those..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like it would be much more difficult to implement/develop coherent semantics for.

```rs
pub fn print<T>(val: T)
where
T: Debug
{ .. }
```
and you want to call that from a less restricted generic
```rs
fn less_restricted<T>(val: T) {
print(val); // error[E0277]: `T` doesn't implement `Debug`
}
```
may it be from a library where you can't restrict it further but know that you only pass in correct types, you could assume the `T: Debug` condition on the upper function:
```rs
pub fn print<T>(val: T)
where
#[unsafe(assume)] T: Debug
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of making the attribute unsafe, would it not make more sense to require the function it is applied to to be unsafe?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well not really.. The user assures the safety by already writing unsafe in the attribute. When we also allow that for bounds in e.g. struct definitions, then there is no way of making that unsafe otherwise. Also, not every function using that must be unsafe, e.g. a type_id_of_static where it get's the typeid of it when it would be 'static would be completely safe.

Copy link
Member

@kennytm kennytm Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DasLixou

Well not really.. The user assures the safety by already writing unsafe in the attribute.

According to the RFC, only the declarator of print knows that T: Debug is an unsafe assumption. The actual user, the caller, does not know this requirement:

  • This makes T still behave like having Debug in the function body, but that isn't the case for the caller. There, it skips the check and just assumes the condition is true.

That means a caller can write this without any unsafe {} to indicate trusted assumption existed:

struct OnlyDebugIfStatic<'a>(&'a u8);
impl Debug for OnlyDebugIfStatic<'static> { ... }

...

let non_static = 0u8;
your_buggy_crate::print(OnlyDebugIfStatic(&non_static)); // UB without any `unsafe` in sight.

When we also allow that for bounds in e.g. struct definitions, then there is no way of making that unsafe otherwise.

That means this feature is flawed

{ .. }
```
This makes `T` still behave like having `Debug` in the function body, but that isn't the case for the caller. \
There, it skips the check and just assumes the condition is true.

You may of course not want to change the `print` function, so you could also make a small util function just for the `less_restricted` call, like so:
```rs
fn less_restricted<T>(val: T) {
fn print_assumed<T>(val: T)
where
#[unsafe(assume)] T: Debug
{
print(val);
}

print_assumed(val); // works fine
}
```


# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

`assume`d bounds are just skipped during bounds check and we trust the user.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work?

unsafe fn foo<T>(param: T) -> impl Debug 
where
    #[unsafe(assume)] T: Debug,
{
    param
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should. Is this something I should explicitly provide as an example?


Later, the compiler could assist with some wrong conditions, like if for example I would pass something in here which doesn't implement `Debug`, the compiler could tell me post-monomorph that this assumed trait bound is not fulfilled for that _specific_ type. But you shouldn't 100% depend on this, as for example lifetimes aren't preserved up to that stage, so any lifetime-dependant condition is completely unchecked, thus making it `unsafe`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just a lint, or might it become a hard error? (Hard errors could cause problems if people are using this feature in dead code.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait is it a problem because it might error without being used or because it is eliminated before being errored so that there's no error?

Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it might error without being used

This one. E.g., code like this:

if check_that_its_really_debug() {
    unsafe { assume_t_implements_debug::<T>() };
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh that's what you mean by dead code.. yeah then hint is probably better, if not even less..


# Drawbacks
[drawbacks]: #drawbacks

Using it is a pretty risky thing and it could also lead some people to prefer using that instead of writing one or two more bounds, but having it there as unsafe is still important I think.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

While it makes signatures higher up the call stack not as strict as they could be, I think having this as a possibility is still important, especially for more complex higher ranked bounds.

# Prior art
[prior-art]: #prior-art

See the [discussion on the internals forum](https://internals.rust-lang.org/t/giving-generics-traits-via-unsafe-code/22753)

# Unresolved questions
[unresolved-questions]: #unresolved-questions


# Future possibilities
[future-possibilities]: #future-possibilities

In a "perfect" world where the compiler could access lifetimes in monomorph, one could remove the `unsafe`, tho that would still make the signatures higher up the call stack less strict.