-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: master
Are you sure you want to change the base?
Conversation
this sounds like just adding late-checked bounds, which isn't necessarily unsafe since the compiler could in theory still do all bounds checking at monomorphization time (more or less exactly how C++ templates work), but it does lead to the almost totally unreadable error messages that C++ templates are infamous for. |
As mentioned in the RFC, it could help catch some errors, but not all and definitely shouldn't be used by ease, thus unreadable errors aren't really that much of a problem.
can't be proven or disproven anymore. |
|
||
`assume`d bounds are just skipped during bounds check and we trust the user. | ||
|
||
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`. |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>() };
}
There was a problem hiding this comment.
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..
yes, hence why I said theoretically, since, to do full checking, the compiler would have to be rearchitected to keep lifetimes around until monomorphization (which is very unlikely to happen). |
It's not, because not having a post-mono check means you are not restricted in what you can do in dead code. |
this is already possible using (full) specialization and I'd argue it is better to evaluate this under a feature which we had experience. #![feature(specialization)]
use std::fmt::Display;
fn print<T: Display>(val: T) {
println!("good! {val}");
}
fn less_restricted<T>(val: T) {
trait PrintAssumed {
fn print_assumed(self);
}
impl<X> PrintAssumed for X {
default fn print_assumed(self) {
unsafe extern "C" {
#[link_name = "\n\n[ERROR] less_restricted() called without satisfying T: Display\n\n"]
fn error();
}
unsafe {
error();
}
}
}
impl<X: Display> PrintAssumed for X {
fn print_assumed(self) {
print(self)
}
}
PrintAssumed::print_assumed(val)
}
fn main() {
print(5);
less_restricted(5);
print("hello");
less_restricted("hello");
// print(Some("not display")); // compile error
// less_restricted(Some("not display")); // linker error
struct OnlyDisplayIfStatic<'a>(&'a str);
impl Display for OnlyDisplayIfStatic<'static> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "static: {}", self.0)
}
}
print(OnlyDisplayIfStatic("static"));
less_restricted(OnlyDisplayIfStatic("static"));
let bad = "bad".to_string();
// print(OnlyDisplayIfStatic(&bad)); // compile error
less_restricted(OnlyDisplayIfStatic(&bad)); // pass, UB.
} |
Interesting idea, but especially with your Edit: oh yeah sorry you even wrote UB there, skipped that |
It is no different from this RFC itself, which you can't prevent anyone using
So |
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
When implementing a function with a `where` clause, like this one: |
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
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..
There was a problem hiding this comment.
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.
@kennytm yeah it isn't very different from the RFC but the problem with specialization is that the thing causing UB isn't causing UB while being unsafe, but rather while being unsound. So how this will work out solely relies on how specialization progresses |
I'm a bit confused on the benefit of these bounds at all. Like, in what circumstances would it be useful to have them either over a regular bound, or no bound at all? |
Only on rare occasions, like where the compiler can't verify it itself because of too many indirections or when you really don't want to go through 30 layers of generic functions |
It would be nice to see a concrete, real-world example. |
This is a feature with very major impact on the type system of the Rust language, and such features are not added lightly. The RFC is very short, containing only a short motivation with very few details. With this, it's hard to extract what exactly the problem it is you're having, and what other solutions there can be. One major feature that relates to this is "implied bounds", RFC Tracking Issue. It's not guaranteed that this will ever land either, but if you do want to solve your problem, that direction seems a lot more promising, so it's probably better to invest your time there instead of pursuing this RFC, which is likely a dead-end (I am not on a relevant team to make the final call about this, but I can't imagine a world in which this is accepted as-is today). This is an area with many hidden complexities, so working on it will not yield immediate returns as things like this take time, but if you want to work on this, I really recommend looking into alternative approaches like implied bounds, or entirely different directions you may come up with. |
One possible use case for this is expressing bounds that the compiler can’t understand yet. For example: unsafe fn foo<T>(param: T)
where
// We actually only need `T: for<'a, 'b: 'a> Trait<'a, 'b>`,
// but rustc can’t understand that atm
#[unsafe(assume)] T: for<'a, 'b> Trait<'a, 'b>
{
...
} |
```rs | ||
pub fn print<T>(val: T) | ||
where | ||
#[unsafe(assume)] T: Debug |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
`assume`d bounds are just skipped during bounds check and we trust the user. |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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?
I think that whether or not the attribute should be unsafe is less important than first defining what unsafe means here. In practice if the point is complex bounds--especially if the point is complex higher-ranked bounds for lifetimes--there's no way you'd be able to spot whether it's safe by reading the code. "you, the human, are now the compiler" isn't a good idea (source: I know C++ and was the human compiler checking lifetimes there...). For things which aren't higher-ranked you can usually get named centralized bounds:
Which has the added benefit of not requiring you to repeat yourself and also plays nice with feature flags. It does work with lifetimes too. It may work to some limited extent with HRTBs but I've never been crazy enough to try. It can even go in the public API of your crate. For the non-HRTB cases this works and prevents marching up and down the call graph if you need to change a bound. |
Implied bounds do look interesting, and I might be able to bend them to my usecase. As for the real world example, I want the user to have many nested functions with a signature looking something like |
This propsal adds support for
#[unsafe(assume)]
-ing conditions inwhere
clauses to help with complex generic call stacks and hinting for higher ranked bounds.Rendered