From 885fd799716c46dfe1e204b16411ff1c98f2db8d Mon Sep 17 00:00:00 2001 From: Boxy Date: Sun, 5 Jan 2025 11:41:45 +0000 Subject: [PATCH] Split stuff out of representing types, and rewrite early/late bound chapter (#2192) --- src/SUMMARY.md | 9 +- ...early-late-bound-implementation-nuances.md | 197 -------- .../turbofishing-and-early-late-bound.md | 125 ------ src/early_late_parameters.md | 424 ++++++++++++++++++ src/generic_parameters_summary.md | 40 +- src/ty_module/early_binder.md | 8 +- 6 files changed, 433 insertions(+), 370 deletions(-) delete mode 100644 src/early-late-bound-params/early-late-bound-implementation-nuances.md delete mode 100644 src/early-late-bound-params/turbofishing-and-early-late-bound.md create mode 100644 src/early_late_parameters.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 2fbbb187d..a8fddbc75 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -134,14 +134,13 @@ - [Prologue](./part-4-intro.md) - [Generic parameter definitions](./generic_parameters_summary.md) - - [Implementation nuances of early/late bound parameters](./early-late-bound-params/early-late-bound-implementation-nuances.md) - - [Interactions with turbofishing](./early-late-bound-params/turbofishing-and-early-late-bound.md) + - [`EarlyBinder` and instantiating parameters](./ty_module/early_binder.md) +- [Binders and Higher ranked regions](./ty_module/binders.md) + - [Instantiating binders](./ty_module/instantiating_binders.md) +- [Early vs Late bound parameters](./early_late_parameters.md) - [The `ty` module: representing types](./ty.md) - [ADTs and Generic Arguments](./ty_module/generic_arguments.md) - [Parameter types/consts/regions](./ty_module/param_ty_const_regions.md) - - [`EarlyBinder` and instantiating parameters](./ty_module/early_binder.md) - - [`Binder` and Higher ranked regions](./ty_module/binders.md) - - [Instantiating binders](./ty_module/instantiating_binders.md) - [`TypeFolder` and `TypeFoldable`](./ty-fold.md) - [Parameter Environments](./param_env/param_env_summary.md) - [What is it?](./param_env/param_env_what_is_it.md) diff --git a/src/early-late-bound-params/early-late-bound-implementation-nuances.md b/src/early-late-bound-params/early-late-bound-implementation-nuances.md deleted file mode 100644 index 01569e8dc..000000000 --- a/src/early-late-bound-params/early-late-bound-implementation-nuances.md +++ /dev/null @@ -1,197 +0,0 @@ -# Early and Late Bound Parameter Implementation Nuances - -> Note: this chapter makes reference to information discussed later on in the [representing types][ch_representing_types] chapter. Specifically, it uses concise notation to represent some more complex kinds of types that have not yet been discussed, such as inference variables. - -[ch_representing_types]: ../ty.md - -Understanding this page likely requires a rudimentary understanding of higher ranked -trait bounds/`for<'a>`and also what types such as `dyn for<'a> Trait<'a>` and - `for<'a> fn(&'a u32)` mean. Reading [the nomincon chapter](https://doc.rust-lang.org/nomicon/hrtb.html) -on HRTB may be useful for understanding this syntax. The meaning of `for<'a> fn(&'a u32)` -is incredibly similar to the meaning of `T: for<'a> Trait<'a>`. - -## What does it mean for parameters to be early or late bound - -All function definitions conceptually have a ZST (this is represented by `TyKind::FnDef` in rustc). -The only generics on this ZST are the early bound parameters of the function definition. e.g. -```rust -fn foo<'a>(_: &'a u32) {} - -fn main() { - let b = foo; - // ^ `b` has type `FnDef(foo, [])` (no args because `'a` is late bound) - assert!(std::mem::size_of_val(&b) == 0); -} -``` - -In order to call `b` the late bound parameters do need to be provided, these are inferred at the -call site instead of when we refer to `foo`. -```rust -fn main() { - let b = foo; - let a: &'static u32 = &10; - foo(a); - // the lifetime argument for `'a` on `foo` is inferred at the callsite - // the generic parameter `'a` on `foo` is inferred to `'static` here -} -``` - -Because late bound parameters are not part of the `FnDef`'s args this allows us to prove trait -bounds such as `F: for<'a> Fn(&'a u32)` where `F` is `foo`'s `FnDef`. e.g. -```rust -fn foo_early<'a, T: Trait<'a>>(_: &'a u32, _: T) {} -fn foo_late<'a, T>(_: &'a u32, _: T) {} - -fn accepts_hr_func Fn(&'a u32, u32)>(_: F) {} - -fn main() { - // doesn't work, the instantiated bound is `for<'a> FnDef<'?0>: Fn(&'a u32, u32)` - // `foo_early` only implements `for<'a> FnDef<'a>: Fn(&'a u32, u32)`- the lifetime - // of the borrow in the function argument must be the same as the lifetime - // on the `FnDef`. - accepts_hr_func(foo_early); - - // works, the instantiated bound is `for<'a> FnDef: Fn(&'a u32, u32)` - accepts_hr_func(foo_late); -} - -// the builtin `Fn` impls for `foo_early` and `foo_late` look something like: -// `foo_early` -impl<'a, T: Trait<'a>> Fn(&'a u32, T) for FooEarlyFnDef<'a, T> { ... } -// `foo_late` -impl<'a, T> Fn(&'a u32, T) for FooLateFnDef { ... } - -``` - -Early bound parameters are present on the `FnDef`. Late bound generic parameters are not present -on the `FnDef` but are instead constrained by the builtin `Fn*` impl. - -The same distinction applies to closures. Instead of `FnDef` we are talking about the anonymous -closure type. Closures are [currently unsound](https://github.com/rust-lang/rust/issues/84366) in -ways that are closely related to the distinction between early/late bound -parameters (more on this later) - -The early/late boundness of generic parameters is only relevant for the desugaring of -functions/closures into types with builtin `Fn*` impls. It does not make sense to talk about -in other contexts. - -The `generics_of` query in rustc only contains early bound parameters. In this way it acts more -like `generics_of(my_func)` is the generics for the FnDef than the generics provided to the function -body although it's not clear to the author of this section if this was the actual justification for -making `generics_of` behave this way. - -## What parameters are currently late bound - -Below are the current requirements for determining if a generic parameter is late bound. It is worth -keeping in mind that these are not necessarily set in stone and it is almost certainly possible to -be more flexible. - -### Must be a lifetime parameter - -Rust can't support types such as `for dyn Trait` or `for fn(T)`, this is a -fundamental limitation of the language as we are required to monomorphize type/const -parameters and cannot do so behind dynamic dispatch. (technically we could probably -support `for dyn MarkerTrait` as there is nothing to monomorphize) - -Not being able to support `for dyn Trait` resulted in making all type and const -parameters early bound. Only lifetime parameters can be late bound. - -### Must not appear in the where clauses - -In order for a generic parameter to be late bound it must not appear in any where clauses. -This is currently an incredibly simplistic check that causes lifetimes to be early bound even -if the where clause they appear in are always true, or implied by well formedness of function -arguments. e.g. -```rust -fn foo1<'a: 'a>(_: &'a u32) {} -// ^^ early bound parameter because it's in a `'a: 'a` clause -// even though the bound obviously holds all the time -fn foo2<'a, T: Trait<'a>(a: T, b: &'a u32) {} -// ^^ early bound parameter because it's used in the `T: Trait<'a>` clause -fn foo3<'a, T: 'a>(_: &'a T) {} -// ^^ early bound parameter because it's used in the `T: 'a` clause -// even though that bound is implied by wellformedness of `&'a T` -fn foo4<'a, 'b: 'a>(_: Inv<&'a ()>, _: Inv<&'b ()>) {} -// ^^ ^^ ^^^ note: -// ^^ ^^ `Inv` stands for `Invariant` and is used to -// ^^ ^^ make the type parameter invariant. This -// ^^ ^^ is necessary for demonstration purposes as -// ^^ ^^ `for<'a, 'b> fn(&'a (), &'b ())` and -// ^^ ^^ `for<'a> fn(&'a u32, &'a u32)` are subtypes- -// ^^ ^^ of each other which makes the bound trivially -// ^^ ^^ satisfiable when making the fnptr. `Inv` -// ^^ ^^ disables this subtyping. -// ^^ ^^ -// ^^^^^^ both early bound parameters because they are present in the -// `'b: 'a` clause -``` - -The reason for this requirement is that we cannot represent the `T: Trait<'a>` or `'a: 'b` clauses -on a function pointer. `for<'a, 'b> fn(Inv<&'a ()>, Inv<&'b ()>)` is not a valid function pointer to -represent`foo4` as it would allow calling the function without `'b: 'a` holding. - -### Must be constrained by where clauses or function argument types - -The builtin impls of the `Fn*` traits for closures and `FnDef`s cannot not have any unconstrained -parameters. For example the following impl is illegal: -```rust -impl<'a> Trait for u32 { type Assoc = &'a u32; } -``` -We must not end up with a similar impl for the `Fn*` traits e.g. -```rust -impl<'a> Fn<()> for FnDef { type Assoc = &'a u32 } -``` - -Violating this rule can trivially lead to unsoundness as seen in [#84366](https://github.com/rust-lang/rust/issues/84366). -Additionally if we ever support late bound type params then an impl like: -```rust -impl Fn<()> for FnDef { type Assoc = T; } -``` -would break the compiler in various ways. - -In order to ensure that everything functions correctly, we do not allow generic parameters to -be late bound if it would result in a builtin impl that does not constrain all of the generic -parameters on the builtin impl. Making a generic parameter be early bound trivially makes it be -constrained by the builtin impl as it ends up on the self type. - -Because of the requirement that late bound parameters must not appear in where clauses, checking -this is simpler than the rules for checking impl headers constrain all the parameters on the impl. -We only have to ensure that all late bound parameters appear at least once in the function argument -types outside of an alias (e.g. an associated type). - -The requirement that they not indirectly be in the args of an alias for it to count is the -same as why the follow code is forbidden: -```rust -impl OtherTrait for ::Assoc { type Assoc = T } -``` -There is no guarantee that `::Assoc` will normalize to different types for every -instantiation of `T`. If we were to allow this impl we could get overlapping impls and the -same is true of the builtin `Fn*` impls. - -## Making more generic parameters late bound - -It is generally considered desirable for more parameters to be late bound as it makes -the builtin `Fn*` impls more flexible. Right now many of the requirements for making -a parameter late bound are overly restrictive as they are tied to what we can currently -(or can ever) do with fn ptrs. - -It would be theoretically possible to support late bound params in `where`-clauses in the -language by introducing implication types which would allow us to express types such as: -`for<'a, 'b: 'a> fn(Inv<&'a u32>, Inv<&'b u32>)` which would ensure `'b: 'a` is upheld when -calling the function pointer. - -It would also be theoretically possible to support it by making the coercion to a fn ptr -instantiate the parameter with an infer var while still allowing the FnDef to not have the -generic parameter present as trait impls are perfectly capable of representing the where clauses -on the function on the impl itself. This would also allow us to support late bound type/const -vars allowing bounds like `F: for Fn(T)` to hold. - -It is almost somewhat unclear if we can change the `Fn` traits to be structured differently -so that we never have to make a parameter early bound just to make the builtin impl have all -generics be constrained. Of all the possible causes of a generic parameter being early bound -this seems the most difficult to remove. - -Whether these would be good ideas to implement is a separate question- they are only brought -up to illustrate that the current rules are not necessarily set in stone and a result of -"its the only way of doing this". - diff --git a/src/early-late-bound-params/turbofishing-and-early-late-bound.md b/src/early-late-bound-params/turbofishing-and-early-late-bound.md deleted file mode 100644 index 6fc30c676..000000000 --- a/src/early-late-bound-params/turbofishing-and-early-late-bound.md +++ /dev/null @@ -1,125 +0,0 @@ -# Turbofishing's interactions with early/late bound parameters - -> Note: this chapter makes reference to information discussed later on in the [representing types][ch_representing_types] chapter. Specifically, it uses concise notation to represent some more complex kinds of types that have not yet been discussed, such as inference variables. - -[ch_representing_types]: ../ty.md - -The early/late bound parameter distinction on functions introduces some complications -when providing generic arguments to functions. This document discusses what those are -and how they might interact with future changes to make more things late bound. - -## Can't turbofish generic arguments on functions sometimes - -When a function has any late bound lifetime parameters (be they explicitly defined or -implicitly introduced via lifetime elision) we disallow specifying any lifetime arguments -on the function. Sometimes this is a hard error other times it is a future compat lint -([`late_bound_lifetime_arguments`](https://github.com/rust-lang/rust/issues/42868)). - -```rust -fn early<'a: 'a>(a: &'a ()) -> &'a () { a } -fn late<'a>(a: &'a ()) -> &'a () { a } - -fn mixed<'a, 'b: 'b>(a: &'a (), b: &'b ()) -> &'a () { a } - -struct Foo; -impl Foo { - fn late<'a>(self, a: &'a ()) -> &'a () { a } -} - -fn main() { - // fine - let f = early::<'static>; - - // some variation of hard errors and future compat lints - Foo.late::<'static>(&()); - let f = late::<'static>; - let f = mixed::<'static, 'static>; - let f = mixed::<'static>; - late::<'static>(&()); -} -``` - -The justification for this is that late bound parameters are not present on the -`FnDef` so the arguments to late bound parameters can't be present in the generic arguments -for the type. i.e. the `late` function in the above code snippet would not have -any generic parameters on the `FnDef` zst: -```rust -// example desugaring of the `late` function and its zst + builtin Fn impl -struct LateFnDef; -impl<'a> Fn<(&'a ())> for LateFnDef { - type Output = &'a (); - ... -} -``` - -The cause for some situations giving future compat lints and others giving hard errors -is a little arbitrary but explainable: -- It's always a hard error for method calls -- It's only a hard error on paths to free functions if there is no unambiguous way to -create the generic arguments for the fndef from the lifetime arguments. (i.e. the amount of -lifetimes provided must be exactly equal to the amount of early bound lifetimes or -else it's a hard error) - -## Back compat issues from turning early bound to late bound - -Because of the previously mentioned restriction on turbofishing generic arguments, it -is a breaking change to upgrade a lifetime from early bound to late bound as it can cause -existing turbofishies to become hard errors/future compat lints. - -Many t-types members have expressed interest in wanting more parameters to be late bound. -We cannot do so if making something late bound is going to break code that many would -expect to work (judging by the future compat lint issue many people do expect to be able -to turbofish late bound parameters). - -## Interactions with late bound type/const parameters - -If we were to make some type/const parameters late bound we would definitely not want -to disallow turbofishing them as it presumably(?) would break a Tonne of code. - -While lifetimes do differ from type/consts in some ways I(BoxyUwU) do not believe there -is any justification for why it would make sense to allow turbofishing late bound -type/const parameters but not late bound lifetimes. - -## Removing the hard error/fcw - -From reasons above it seems reasonable that we may want to remove the hard error and fcw -(removing the errors/fcw is definitely a blocker for making more things late bound). - -example behaviour: -```rust -fn late<'a>(a: &'a ()) -> &'a () { a } - -fn accepts_fn(_: impl for<'a> Fn(&'a ()) -> &'a ()) {} -fn accepts_fn_2(_: impl Fn(&'static ()) -> &'static ()) {} - -fn main() { - let f = late::<'static>; - - accepts_fn(f); //~ error: `f` doesn't implement `for<'a> Fn(&'a ()) -> &'a ()` - accepts_fn_2(f) // works - - accepts_fn(late) // works -} -```` - -one potential complication is that we would want a way to specify a generic argument -to a function without having to specify arguments for all previous parameters. i.e. -ideally you could write the following code somehow. -```rust -fn late<'a, 'b>(_: &'a (), _: &'b ()) {} - -fn accepts_fn(_: impl for<'a> Fn(&'a (), &'static ())) {} - -fn main() { - // a naive implementation would have an inference variable as - // the argument to the `'a` parameter no longer allowing the `FnDef` - // to satisfy the bound `for<'a> Fn(&'a ())` - let f = late::<'_, 'static>; - accepts_fn(f); -} -``` -Maybe we can just special case HIR ty lowering for `_`/`'_` arguments for late bound -parameters somehow and have it not mean the same thing as `_` for early bound parameters. -Regardless I think we would need a solution that would allow writing the above code even -if it was done by some new syntax such as having to write `late::` -(naturally `k#no_argument` would only make sense as an argument to late bound parameters). diff --git a/src/early_late_parameters.md b/src/early_late_parameters.md new file mode 100644 index 000000000..6d1365529 --- /dev/null +++ b/src/early_late_parameters.md @@ -0,0 +1,424 @@ + +# Early vs Late bound parameters + + + +> **NOTE**: This chapter largely talks about early/late bound as being solely relevant when discussing function item types/function definitions. This is potentially not completely true, async blocks and closures should likely be discussed somewhat in this chapter. + +## What does it mean to be "early" bound or "late" bound + +Every function definition has a corresponding ZST that implements the `Fn*` traits known as a [function item type][function_item_type]. This part of the chapter will talk a little bit about the "desugaring" of function item types as it is useful context for explaining the difference between early bound and late bound generic parameters. + +Let's start with a very trivial example involving no generic parameters: + +```rust +fn foo(a: String) -> u8 { + # 1 + /* snip */ +} +``` + +If we explicitly wrote out the definitions for the function item type corresponding to `foo` and its associated `Fn` impl it would look something like this: +```rust,ignore +struct FooFnItem; + +impl Fn<(String,)> for FooFnItem { + type Output = u8; + /* fn call(&self, ...) -> ... { ... } */ +} +``` + +The builtin impls for the `FnMut`/`FnOnce` traits as well as the impls for `Copy` and `Clone` were omitted for brevity reasons (although these traits *are* implemented for function item types). + +A slightly more complicated example would involve introducing generic parameters to the function: +```rust +fn foo(a: T) -> T { + # a + /* snip */ +} +``` +Writing out the definitions would look something like this: +```rust,ignore +struct FooFnItem(PhantomData T>); + +impl Fn<(T,)> for FooFnItem { + type Output = T; + /* fn call(&self, ...) -> ... { ... } */ +} +``` + +Note that the function item type `FooFnItem` is generic over some type parameter `T` as defined on the function `foo`. However, not all generic parameters defined on functions are also defined on the function item type as demonstrated here: +```rust +fn foo<'a, T: Sized>(a: &'a T) -> &'a T { + # a + /* snip */ +} +``` +With its "desugared" form looking like so: +```rust,ignore +struct FooFnItem(PhantomData fn(&'a T) -> &'a T>); + +impl<'a, T: Sized> Fn<(&'a T,)> for FooFnItem { + type Output = &'a T; + /* fn call(&self, ...) -> ... { ... } */ +} +``` + +The lifetime parameter `'a` from the function `foo` is not present on the function item type `FooFnItem` and is instead introduced on the builtin impl solely for use in representing the argument types. + +Generic parameters not all being defined on the function item type means that there are two steps where generic arguments are provided when calling a function. +1. Naming the function (e.g. `let a = foo;`) the arguments for `FooFnItem` are provided. +2. Calling the function (e.g. `a(&10);`) any parameters defined on the builtin impl are provided. + +This two-step system is where the early vs late naming scheme comes from, early bound parameters are provided in the *earliest* step (naming the function), whereas late bound parameters are provided in the *latest* step (calling the function). + +Looking at the desugaring from the previous example we can tell that `T` is an early bound type parameter and `'a` is a late bound lifetime parameter as `T` is present on the function item type but `'a` is not. See this example of calling `foo` annotated with where each generic parameter has an argument provided: +```rust +fn foo<'a, T: Sized>(a: &'a T) -> &'a T { + # a + /* snip */ +} + +// Here we provide a type argument `String` to the +// type parameter `T` on the function item type +let my_func = foo::; + +// Here (implicitly) a lifetime argument is provided +// to the lifetime parameter `'a` on the builtin impl. +my_func(&String::new()); +``` + +[function_item_type]: https://doc.rust-lang.org/reference/types/function-item.html + +## Differences between early and late bound parameters + +### Higher ranked function pointers and trait bounds + +A generic parameter being late bound allows for more flexible usage of the function item. For example if we have some function `foo` with an early bound lifetime parameter and some function `bar` with a late bound lifetime parameter `'a` we would have the following builtin `Fn` impls: +```rust,ignore +impl<'a> Fn<(&'a String,)> for FooFnItem<'a> { /* ... */ } +impl<'a> Fn<(&'a String,)> for BarFnItem { /* ... */ } +``` + +The `bar` function has a strictly more flexible signature as the function item type can be called with a borrow with *any* lifetime, whereas the `foo` function item type would only be callable with a borrow with the same lifetime on the function item type. We can show this by simply trying to call `foo`'s function item type multiple times with different lifetimes: + +```rust +// The `'a: 'a` bound forces this lifetime to be early bound. +fn foo<'a: 'a>(b: &'a String) -> &'a String { b } +fn bar<'a>(b: &'a String) -> &'a String { b } + +// Early bound generic parameters are instantiated here when naming +// the function `foo`. As `'a` is early bound an argument is provided. +let f = foo::<'_>; + +// Both function arguments are required to have the same lifetime as +// the lifetime parameter being early bound means that `f` is only +// callable for one specific lifetime. +// +// As we call this with borrows of different lifetimes, the borrow checker +// will error here. +f(&String::new()); +f(&String::new()); +``` + +In this example we call `foo`'s function item type twice, each time with a borrow of a temporary. These two borrows could not possible have lifetimes that overlap as the temporaries are only alive during the function call, not after. The lifetime parameter on `foo` being early bound requires all callers of `f` to provide a borrow with the same lifetime, as this is not possible the borrow checker errors. + +If the lifetime parameter on `foo` was late bound this would be able to compile as each caller could provide a different lifetime argument for its borrow. See the following example which demonstrates this using the `bar` function defined above: + +```rust +#fn foo<'a: 'a>(b: &'a String) -> &'a String { b } +#fn bar<'a>(b: &'a String) -> &'a String { b } + +// Early bound parameters are instantiated here, however as `'a` is +// late bound it is not provided here. +let b = bar; + +// Late bound parameters are instantiated separately at each call site +// allowing different lifetimes to be used by each caller. +b(&String::new()); +b(&String::new()); +``` + +This is reflected in the ability to coerce function item types to higher ranked function pointers and prove higher ranked `Fn` trait bounds. We can demonstrate this with the following example: +```rust +// The `'a: 'a` bound forces this lifetime to be early bound. +fn foo<'a: 'a>(b: &'a String) -> &'a String { b } +fn bar<'a>(b: &'a String) -> &'a String { b } + +fn accepts_hr_fn(_: impl for<'a> Fn(&'a String) -> &'a String) {} + +fn higher_ranked_trait_bound() { + let bar_fn_item = bar; + accepts_hr_fn(bar_fn_item); + + let foo_fn_item = foo::<'_>; + // errors + accepts_hr_fn(foo_fn_item); +} + +fn higher_ranked_fn_ptr() { + let bar_fn_item = bar; + let fn_ptr: for<'a> fn(&'a String) -> &'a String = bar_fn_item; + + let foo_fn_item = foo::<'_>; + // errors + let fn_ptr: for<'a> fn(&'a String) -> &'a String = foo_fn_item; +} +``` + +In both of these cases the borrow checker errors as it does not consider `foo_fn_item` to be callable with a borrow of any lifetime. This is due to the fact that the lifetime parameter on `foo` is early bound, causing `foo_fn_item` to have a type of `FooFnItem<'_>` which (as demonstrated by the desugared `Fn` impl) is only callable with a borrow of the same lifetime `'_`. + +### Turbofishing in the presence of late bound parameters + +As mentioned previously, the distinction between early and late bound parameters means that there are two places where generic parameters are instantiated: +- When naming a function (early) +- When calling a function (late) + +There currently is no syntax for explicitly specifying generic arguments for late bound parameters as part of the call step, only specifying generic arguments when naming a function. The syntax `foo::<'static>();`, despite being part of a function call, behaves as `(foo::<'static>)();` and instantiates the early bound generic parameters on the function item type. + +See the following example: +```rust +fn foo<'a>(b: &'a u32) -> &'a u32 { b } + +let f /* : FooFnItem */ = foo::<'static>; +``` + +The above example errors as the lifetime parameter `'a` is late bound and so cannot be instantiated as part of the "naming a function" step. If we make the lifetime parameter early bound we will see this code start to compile: +```rust +fn foo<'a: 'a>(b: &'a u32) -> &'a u32 { b } + +let f /* : FooFnItem<'static> */ = foo::<'static>; +``` + +What the current implementation of the compiler aims to do is error when specifying lifetime arguments to a function that has both early *and* late bound lifetime parameters. In practice, due to excessive breakage, some cases are actually only future compatibility warnings ([#42868](https://github.com/rust-lang/rust/issues/42868)): +- When the amount of lifetime arguments is the same as the number of early bound lifetime parameters a FCW is emitted instead of an error +- An error is always downgraded to a FCW when using method call syntax + +To demonstrate this we can write out the different kinds of functions and give them both a late and early bound lifetime: +```rust,ignore +fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} + +struct Foo; + +trait Trait: Sized { + fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()); + fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()); +} + +impl Trait for Foo { + fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {} + fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} +} + +impl Foo { + fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {} + fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} +} +``` + +Then, for the first case, we can call each function with a single lifetime argument (corresponding to the one early bound lifetime parameter) and note that it only results in a FCW rather than a hard error. +```rust +#![deny(late_bound_lifetime_arguments)] + +#fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} +# +#struct Foo; +# +#trait Trait: Sized { +# fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()); +# fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()); +#} +# +#impl Trait for Foo { +# fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {} +# fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} +#} +# +#impl Foo { +# fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {} +# fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} +#} +# +// Specifying as many arguments as there are early +// bound parameters is always a future compat warning +Foo.trait_method::<'static>(&(), &()); +Foo::trait_method::<'static>(Foo, &(), &()); +Foo::trait_function::<'static>(&(), &()); +Foo.inherent_method::<'static>(&(), &()); +Foo::inherent_function::<'static>(&(), &()); +free_function::<'static>(&(), &()); +``` + +For the second case we call each function with more lifetime arguments than there are lifetime parameters (be it early or late bound) and note that method calls result in a FCW as opposed to the free/associated functions which result in a hard error: +```rust +#fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} +# +#struct Foo; +# +#trait Trait: Sized { +# fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()); +# fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()); +#} +# +#impl Trait for Foo { +# fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {} +# fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} +#} +# +#impl Foo { +# fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {} +# fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {} +#} +# +// Specifying more arguments than there are early +// bound parameters is a future compat warning when +// using method call syntax. +Foo.trait_method::<'static, 'static, 'static>(&(), &()); +Foo.inherent_method::<'static, 'static, 'static>(&(), &()); +// However, it is a hard error when not using method call syntax. +Foo::trait_method::<'static, 'static, 'static>(Foo, &(), &()); +Foo::trait_function::<'static, 'static, 'static>(&(), &()); +Foo::inherent_function::<'static, 'static, 'static>(&(), &()); +free_function::<'static, 'static, 'static>(&(), &()); +``` + +Even when specifying enough lifetime arguments for both the late and early bound lifetime parameter, these arguments are not actually used to annotate the lifetime provided to late bound parameters. We can demonstrate this by turbofishing `'static` to a function while providing a non-static borrow: +```rust +struct Foo; + +impl Foo { + fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b String ) {} +} + +Foo.inherent_method::<'static, 'static>(&(), &String::new()); +``` + +This compiles even though the `&String::new()` function argument does not have a `'static` lifetime, this is because "extra" lifetime arguments are discarded rather than taken into account for late bound parameters when actually calling the function. + +### Liveness of types with late bound parameters + +When checking type outlives bounds involving function item types we take into account early bound parameters. For example: + +```rust +fn foo(_: T) {} + +fn requires_static(_: T) {} + +fn bar() { + let f /* : FooFnItem */ = foo::; + requires_static(f); +} +``` + +As the type parameter `T` is early bound, the desugaring of the function item type for `foo` would look something like `struct FooFnItem`. Then in order for `FooFnItem: 'static` to hold we must also require `T: 'static` to hold as otherwise we would wind up with soundness bugs. + +Unfortunately, due to bugs in the compiler, we do not take into account early bound *lifetimes*, which is the cause of the open soundness bug [#84366](https://github.com/rust-lang/rust/issues/84366). This means that it's impossible to demonstrate a "difference" between early/late bound parameters for liveness/type outlives bounds as the only kind of generic parameters that are able to be late bound are lifetimes which are handled incorrectly. + +Regardless, in theory the code example below *should* demonstrate such a difference once [#84366](https://github.com/rust-lang/rust/issues/84366) is fixed: +```rust +fn early_bound<'a: 'a>(_: &'a String) {} +fn late_bound<'a>(_: &'a String) {} + +fn requires_static(_: T) {} + +fn bar<'b>() { + let e = early_bound::<'b>; + // this *should* error but does not + requires_static(e); + + let l = late_bound; + // this correctly does not error + requires_static(l); +} +``` + +## Requirements for a parameter to be late bound + +### Must be a lifetime parameter + +Type and Const parameters are not able to be late bound as we do not have a way to support types such as `dyn for Fn(Box)` or `for fn(Box)`. Calling such types requires being able to monomorphize the underlying function which is not possible with indirection through dynamic dispatch. + +### Must not be used in a where clause + +Currently when a generic parameter is used in a where clause it must be early bound. For example: +```rust +# trait Trait<'a> {} +fn foo<'a, T: Trait<'a>>(_: &'a String, _: T) {} +``` + +In this example the lifetime parameter `'a` is considered to be early bound as it appears in the where clause `T: Trait<'a>`. This is true even for "trivial" where clauses such as `'a: 'a` or those implied by wellformedness of function arguments, for example: +```rust +fn foo<'a: 'a>(_: &'a String) {} +fn bar<'a, T: 'a>(_: &'a T) {} +``` + +In both of these functions the lifetime parameter `'a` would be considered to be early bound even though the where clauses they are used in arguably do not actually impose any constraints on the caller. + +The reason for this restriction is a combination of two things: +- We cannot prove bounds on late bound parameters until they have been instantiated +- Function pointers and trait objects do not have a way to represent yet to be proven where clauses from the underlying function + +Take the following example: +```rust +trait Trait<'a> {} +fn foo<'a, T: Trait<'a>>(_: &'a T) {} + +let f = foo::; +let f = f as for<'a> fn(&'a String); +f(&String::new()); +``` + +At *some point* during type checking an error should be emitted for this code as `String` does not implement `Trait` for any lifetime. + +If the lifetime `'a` were late bound then this becomes difficult to check. When naming `foo` we do not know what lifetime should be used as part of the `T: Trait<'a>` trait bound as it has not yet been instantiated. When coercing the function item type to a function pointer we have no way of tracking the `String: Trait<'a>` trait bound that must be proven when calling the function. + +If the lifetime `'a` is early bound (which it is in the current implementation in rustc), then the trait bound can be checked when naming the function `foo`. Requiring parameters used in where clauses to be early bound gives a natural place to check where clauses defined on the function. + +Finally, we do not require lifetimes to be early bound if they are used in *implied bounds*, for example: +```rust +fn foo<'a, T>(_: &'a T) {} + +let f = foo; +f(&String::new()); +f(&String::new()); +``` + +This code compiles, demonstrating that the lifetime parameter is late bound, even though `'a` is used in the type `&'a T` which implicitly requires `T: 'a` to hold. Implied bounds can be treated specially as any types introducing implied bounds are in the signature of the function pointer type, which means that when calling the function we know to prove `T: 'a`. + +### Must be constrained by argument types + +It is important that builtin impls on function item types do not wind up with unconstrained generic parameters as this can lead to unsoundness. This is the same kind of restriction as applies to user written impls, for example the following code results in an error: +```rust +trait Trait { + type Assoc; +} + +impl<'a> Trait for u8 { + type Assoc = &'a String; +} +``` + +The analogous example for builtin impls on function items would be the following: +```rust,ignore +fn foo<'a>() -> &'a String { /* ... */ } +``` +If the lifetime parameter `'a` were to be late bound we would wind up with a builtin impl with an unconstrained lifetime, we can manually write out the desugaring for the function item type and its impls with `'a` being late bound to demonstrate this: +```rust,ignore +// NOTE: this is just for demonstration, in practice `'a` is early bound +struct FooFnItem; + +impl<'a> Fn<()> for FooFnItem { + type Output = &'a String; + /* fn call(...) -> ... { ... } */ +} +``` + +In order to avoid such a situation we consider `'a` to be early bound which causes the lifetime on the impl to be constrained by the self type: +```rust,ignore +struct FooFnItem<'a>(PhantomData &'a String>); + +impl<'a> Fn<()> for FooFnItem<'a> { + type Output = &'a String; + /* fn call(...) -> ... { ... } */ +} +``` \ No newline at end of file diff --git a/src/generic_parameters_summary.md b/src/generic_parameters_summary.md index 3403c9f29..da38ba045 100644 --- a/src/generic_parameters_summary.md +++ b/src/generic_parameters_summary.md @@ -25,42 +25,4 @@ Interestingly, `ty::Generics` does not currently contain _every_ generic paramet [ch_representing_types]: ./ty.md [`ty::Generics`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.Generics.html [`GenericParamDef`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/generics/struct.GenericParamDef.html - -# Early vs Late bound parameters - - -```rust -fn foo<'a, T>(b: &'a T) -> &'a T { b } -// ^^ ^early bound -// ^^ -// ^^late bound -``` - -Generally when referring to an item with generic parameters you must specify a list of generic arguments corresponding to the item's generic parameters. In some cases it is permitted to elide these arguments but still, implicitly, a set of arguments are provided (e.g. `Vec::default()` desugars to `Vec::<_>::default()`). - -For functions this is not necessarily the case, for example if we take the function `foo` from the example above and write the following code: -```rust -fn main() { - let f = foo::<_>; - - let b = String::new(); - let c = String::new(); - - f(&b); - drop(b); - f(&c); -} -``` - -This code compiles perfectly fine even though there is no single lifetime that could possibly be specified in `foo::<_>` that would allow for both -the `&b` and `&c` borrows to be used as arguments (note: the `drop(b)` line forces the `&b` borrow to be shorter than the `&c` borrow). This works because the `'a` lifetime is _late bound_. - -A generic parameter being late bound means that when we write `foo::<_>` we do not actually provide an argument for that parameter, instead we wait until _calling_ the function to provide the generic argument. In the above example this means that we are doing something like `f::<'_>(&b);` and `f::<'_>(&c);` (although in practice we do not actually support turbofishing late bound parameters in this manner) - -It may be helpful to think of "early bound parameter" or "late bound parameter" as meaning "early provided parameter" and "late provided parameter", i.e. we provide the argument to the parameter either early (when naming the function) or late (when calling it). - -Late bound parameters on functions are tracked with a [`Binder`] when accessing the signature of the function, this can be done with the [`fn_sig`] query. For more information of binders see the [chapter on `Binder`s ][ch_binders]. - -[`Binder`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_type_ir/binder/struct.Binder.html -[`fn_sig`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/context/struct.TyCtxt.html#method.fn_sig -[ch_binders]: ./ty_module/binders.md \ No newline at end of file +[ch_binders]: ./ty_module/binders.md diff --git a/src/ty_module/early_binder.md b/src/ty_module/early_binder.md index 69db18935..e8ff3a530 100644 --- a/src/ty_module/early_binder.md +++ b/src/ty_module/early_binder.md @@ -47,10 +47,10 @@ fn bar(foo: Foo) { In the compiler the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), at some point during type checking `bar` we will wind up calling `FieldDef::ty(x, &[u32, f32])` in order to obtain the type of `foo.x`. -**Note on indices:** It is possible for the indices in `Param` to not match with what the `EarlyBinder` binds. For -example, the index could be out of bounds or it could be the index of a lifetime when we were expecting a type. -These sorts of errors would be caught earlier in the compiler when translating from a `rustc_hir::Ty` to a `ty::Ty`. -If they occur later, that is a compiler bug. +**Note on indices:** It is a bug if the index of a `Param` does not match what the `EarlyBinder` binds. For +example, if the index is out of bounds or the index index of a lifetime corresponds to a type parameter. +These sorts of errors are caught earlier in the compiler during name resolution where we disallow references +to generics parameters introduced by items that should not be nameable by the inner item. [`FieldDef::ty`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.FieldDef.html#method.ty [field_def_ty_src]: https://github.com/rust-lang/rust/blob/44d679b9021f03a79133021b94e6d23e9b55b3ab/compiler/rustc_middle/src/ty/mod.rs#L1421-L1426