Skip to content
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

Implement parsing of pinned borrows #135731

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

frank-king
Copy link
Contributor

@frank-king frank-king commented Jan 19, 2025

This PR implements part of #130494.

EDIT: It introduces &pin mut $place and &pin const $place as sugars for std::pin::pin!($place) and its shared reference equivalent, except that $place will not be moved when borrowing. The borrow check will be in charge of enforcing places cannot be moved or mutably borrowed since being pinned till dropped.

Implementation steps:

  • parse the &pin mut $place and &pin const $place syntaxes
  • borrowck of &pin mut|const
  • support autoref of &pin mut|const when needed

@rustbot
Copy link
Collaborator

rustbot commented Jan 19, 2025

r? @petrochenkov

rustbot has assigned @petrochenkov.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot
Copy link
Collaborator

rustbot commented Jan 19, 2025

Some changes occurred to the CTFE machinery

cc @rust-lang/wg-const-eval

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 19, 2025
@rust-log-analyzer

This comment has been minimized.

@rustbot
Copy link
Collaborator

rustbot commented Jan 20, 2025

Some changes occurred in src/tools/rustfmt

cc @rust-lang/rustfmt

@oli-obk oli-obk added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jan 20, 2025
@petrochenkov
Copy link
Contributor

r? compiler

@rustbot rustbot assigned fmease and unassigned petrochenkov Jan 20, 2025
@traviscross
Copy link
Contributor

It introduces &pin const place and &pin mut place as sugars for Pin::new(&place) and unsafe { Pin::new_unchecked(&mut place) }.

This is not the semantic we want or a good placeholder for it. What we want is that &pin mut $place and &pin const $place do safe pinning and safe pin projection. Probably the right placeholder is that &pin mut $place does pin!($place) and &pin const $place does the shared reference equivalent of that. Then, the next extension is to make this also work:

fn f<T: Unpin>(x: &mut T) {
    let _: Pin<&mut T> = &pin mut *x; // i.e., `Pin::new(&mut *x)`.
}

That is, if the type in the place is Unpin, we can skip the precautionary move.

After that, we start talking about how to extend this to safe pin projection to struct fields.

cc @tmandry @eholk

@traviscross
Copy link
Contributor

r? traviscross

@rustbot rustbot assigned traviscross and unassigned fmease Feb 28, 2025
@frank-king
Copy link
Contributor Author

frank-king commented Mar 1, 2025

It introduces &pin const place and &pin mut place as sugars for Pin::new(&place) and unsafe { Pin::new_unchecked(&mut place) }.

This is not the semantic we want or a good placeholder for it. What we want is that &pin mut $place and &pin const $place do safe pinning and safe pin projection. Probably the right placeholder is that &pin mut $place does pin!($place) and &pin const $place does the shared reference equivalent of that.

I agree with you, and my previous statement was not precise. What I mean is similar to yours. I expect &pin mut $place to apply only to pinned places, &mut $place and move $place (move comes from the MIR terminology I think) to apply only to non-pinned places, and they do not intersect with each other.

The pinned places might be defined as:

A place is a pinned place if one of the following conditions holds:

  • It is dereferenced from a Pin<&mut T> (when T: Unpin, it can also be considered as a non-pinned place).
  • It is a binding variable of type T declared by pin mut (I use the feature introduced by Parse pinned local variable declarations #135631). When T: Unpin, it can also be considered as a non-pinned place.
  • It is a temporary (temporaries can be considered as either pinned places or non-pinned places).
  • It is a field access of a pinned place, and the field must have type T that doesn't impl Unpin.
  • It is an indexing (const or non-const) or subslicing of a pinned place.

Here are a few examples to explain:

#[derive(Default)]
struct Foo<T> {
    x: T,
}

fn f<T: Default, U: Default + Unpin>() {
   // Case 1: not `Unpin`, and not pinned
   let mut a = Foo::<T>::default(); // `a` and `a.x` are non-pinned places but not pinned places
   let _: &mut Foo = &mut a; // ok
   let _: &mut T = &mut a.x; // ok
   let _: Pin<&mut Foo> = &pin mut a; // ERROR
   let _: Pin<&mut T> = &pin mut a.x; // ERROR

   // Case 2: not `Unpin`, and pinned
   let pin mut b = Foo::<T>::default(); // `b` and `b.x` are pinned places but not non-pinned places
   let _: &mut Foo = &mut b; // ERROR
   let _: &mut Foo = &mut b.x; // ERROR
   let _: &pin mut Foo = &pin mut b; // ok
   let _: &pin mut Foo = &pin mut b.x; // ok

   // Case 3: `Unpin`, and not pinned
   let mut c = Foo::<U>::default(); // `c` and `c.x` are both pinned and non-pinned places
   let _: &mut Foo = &mut c; // ok
   let _: &mut T = &mut c.x; // ok
   let _: Pin<&mut Foo> = &pin mut c; // ok
   let _: Pin<&mut T> = &pin mut c.x; // ok

   // Case 4: `Unpin`, and pinned
   let pin mut d = Foo::<U>::default(); // WARN (useless `pin`) // `d` and `d.x` are both pinned and non-pinned places
   let _: &mut Foo = &mut d; // ok
   let _: &mut T = &mut d.x; // ok
   let _: Pin<&mut Foo> = &pin mut d; // ok (useless `pin`)
   let _: Pin<&mut T> = &pin mut d.x; // ok (useless `pin`)
}

@traviscross
Copy link
Contributor

traviscross commented Mar 2, 2025

Thanks, that's helpful. Going through the examples, starting on Case 1, we're actually instead interested in this working:

struct Foo<T> { x: T }
fn f<T>(mut x: Foo<T>) {
    let _: Pin<&mut Foo<T>> = &pin mut x; //~ OK
    let _: &Foo<T> = &x; //~ OK
    let _: &mut Foo<T> = &mut x; //~ ERROR place has been pinned
    let _move: Foo<T> = x; //~ ERROR place has been pinned
}

The &pin mut x is allowed even though the type is not Unpin because the borrow checker will track this and then prevent a later mutable reference to or move of the place. Of course, if the type of the place is Unpin, then these can be allowed.

In this way, &pin mut $place then entirely subsumes the pin! macro.

This is also why we're not sold on doing let pin mut, because we suspect we may not need it with these mechanics.

Regarding the safe pin projections to fields, let's do that in a separate PR so we can set those questions aside for the moment. That a place is pinned doesn't necessarily imply that we can safely pin project to a field without taking a number of other steps.

@frank-king
Copy link
Contributor Author

frank-king commented Mar 2, 2025

Thanks for your feedback too! Regarding your code snippet, I have some questions.

First, this is the semantics of pin!($place), where $place is moved after been pinned (playground).

struct Foo<T> { x: T }
fn f<T>(mut x: Foo<T>) {
    let _: Pin<&mut Foo<T>> = pin!(x); //~ OK
    let _: &Foo<T> = &x; //~ ERROR place has been moved
    let _: &mut Foo<T> = &mut x; //~ ERROR place has been moved
    let _move: Foo<T> = x; //~ ERROR place has been moved
}

Then in your version, &pin mut $place acts like pin!($place) but more similar to a normal borrow without moving the place. However, $place is still pinned even if the pinned borrow has expired.

struct Foo<T> { x: T }
fn f<T>(mut x: Foo<T>) {
    let _: Pin<&mut Foo<T>> = &pin mut x; //~ OK
    let _: &Foo<T> = &x; //~ OK
    let _: &mut Foo<T> = &mut x; //~ ERROR place has been pinned
    let _move: Foo<T> = x; //~ ERROR place has been pinned
}

What I expect is, the borrow checker should allow the original place to be mutably borrowed or moved after all pinned borrows expired (if we don't introduce let pin mut to make places pinned explicitly), that is:

fn ignore<T>(_: T) {}
struct Foo<T> { x: T }
fn f<T>(mut x: Foo<T>) {
    {
        let _pinned: Pin<&mut Foo<T>> = &pin mut x; //~ OK
        let _: &Foo<T> = &x; //~ OK
        // Mutable borrows or moves are not allowed during the pinned borrow is alive
        let _: &mut Foo<T> = &mut x; //~ ERROR place has been pinned
        let _move: Foo<T> = x; //~ ERROR place has been pinned

        // Make sure `_pinned` is used
        ignore(_pinned);
    }
    let _: &Foo<T> = &x; //~ OK
    // Mutable borrows or moves are allowed again after the pinned borrow expires
    let _: &mut Foo<T> = &mut x; //~ OK
    let _move: Foo<T> = x; //~ OK
}

@frank-king frank-king force-pushed the feature/pin-borrow branch from e57789f to 1c081ea Compare March 2, 2025 02:39
@traviscross
Copy link
Contributor

traviscross commented Mar 2, 2025

What I expect is, the borrow checker should allow the original place to be mutably borrowed or moved after all pinned borrows expired...

This can't be allowed. The contract with Pin is that once something has been pinned, its memory cannot be invalidated or repurposed until after drop has run, and that implies we cannot allow mutable references to exist to it or for it to be moved, even after that pinned reference is out of scope.

What the borrow checker must do here, then, is to flag the &pin mut place in a way that once the pinned mutable reference is dead it's OK to allow new pinned references or shared ones, but it's not OK to allow a later unpinned mutable reference or a move. So we'd expect, as above:

struct Foo<T> { x: T }
fn f<T>(mut x: Foo<T>) {
    let _: &mut Foo<T> = &mut x; //~ OK
    //~^ Before we pin the thing, it's OK for mutable references to
    //~| exist to it.
    //~|
    //~| Note that the above mutable reference is dead at this point.
    let _: Pin<&mut Foo<T>> = &pin mut x; //~ OK
    //~^ Now we create a pinned mutable reference.  This is allowed
    //~| since the earlier mutable reference is dead.  Once this
    //~| pinned reference is created, we can no longer allow the
    //~| pointed to memory to be invalidated or repurposed until
    //~| after `drop` has been run, even after this pinned reference
    //~| is dead.
    //~|
    //~| Note that the pinned reference is dead at this point.
    let _: &Foo<T> = &x; //~ OK
    //~^ We can allow shared references because they don't allow us
    //~| to invalidate or repurpose the memory.
    let _: Pin<&mut Foo<T>> = &pin mut x; //~ OK
    //~^ We can allow a new pinned mutable reference for the same
    //~| reason.
    let _: Pin<&Foo<T>> = &pin const x; //~ OK
    //~^ Or a new pinned shared reference, for that matter.
    let _: &mut Foo<T> = &mut x; //~ ERROR place has been pinned
    //~^ We can't allow a mutable reference to exist, though, because
    //~| that would violate the contract.
    let _move: Foo<T> = x; //~ ERROR place has been pinned
    //~^ Neither can we allow the once-pinned thing to be moved.
}

What the pin! macro does is enforce this contract in a crude way by forcing a move. With this work, that trick wouldn't (we hope) be necessary, as the borrow checker would directly enforce the pinning rules.

@frank-king
Copy link
Contributor Author

This can't be allowed. The contract with Pin is that once something has been pinned, its memory cannot be invalidated or repurposed until after drop has run, and that implies we cannot allow mutable references to exist to it or for it to be moved, even after that pinned reference is out of scope.

Oh, yes, I forgot that Pin also interact with drops. Then I'd slightly prefer explicitly mark places pinned via let pin mut or else it can be anti-intuitive that &pin mut|const looks like borrows but the pin effect is not tied to the borrow's lifetime. Though it will lose some flexibility that a place must be pinned at creation. (BTW, I personally don't really like the taste that places can be freely moved before being pinned. Anyway, we can still set it aside and revisit it later when doing something like constructing values in place.)

@traviscross
Copy link
Contributor

Yes, the intuition point is something we'll I'm sure consider later. We want to try it first in the way mentioned.

@tmandry
Copy link
Member

tmandry commented Mar 4, 2025

What @traviscross said above sounds right to me. I would also mention that we want dispatch on self: Pin<&mut Self> methods to work with pin autoref, and to have all the same invariants enforced. Since borrow check is implemented on MIR I think it should be agnostic to this, but it's important for the end user story.

@bors
Copy link
Contributor

bors commented Mar 7, 2025

☔ The latest upstream changes (presumably #138114) made this pull request unmergeable. Please resolve the merge conflicts.

@frank-king frank-king force-pushed the feature/pin-borrow branch from 1c081ea to 7010181 Compare March 7, 2025 14:19
@rustbot
Copy link
Collaborator

rustbot commented Mar 7, 2025

Some changes occurred to constck

cc @fee1-dead

@frank-king frank-king changed the title Implement pinned borrows Implement parsing of pinned borrows Mar 7, 2025
@frank-king
Copy link
Contributor Author

@rustbot ready
(I assume that this PR only implements parsing of &pin mut|const syntax, the borrowck and autoref stuffs will be implemented in the next PRs)

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Mar 7, 2025
Comment on lines +342 to +345
ast::BorrowKind::Pin => {
self.word_nbsp("pin");
self.print_mutability(mutability, true);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Like we did over in #135733, let's add the corresponding pretty test.

Comment on lines +1420 to +1423
hir::BorrowKind::Pin => {
self.word_nbsp("pin");
self.print_mutability(mutability, true);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add a //@ pretty-mode:hir test as well.

Comment on lines +691 to +695
hir::BorrowKind::Pin => {
// See comments in the `hir::BorrowKind::Ref` arm above.
let region = self.next_region_var(infer::BorrowRegion(expr.span));
Ty::new_pinned_ref(self.tcx, region, ty, mutbl)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than repeating the region logic here, it'd probably be better to combine the match arms, then match within the arm for the Ty::new_* part.

Comment on lines +475 to +476
// make `&pin mut $expr` and `&pin const $expr` into `Pin { __pointer: &mut $expr }`
// and `Pin { __pointer: &$expr }`
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// make `&pin mut $expr` and `&pin const $expr` into `Pin { __pointer: &mut $expr }`
// and `Pin { __pointer: &$expr }`
// Make `&pin mut $expr` and `&pin const $expr` into
// `Pin { __pointer: &mut $expr }` and `Pin { __pointer: &$expr }`.

Comment on lines +840 to +841
// `pin [ const | mut ]`.
// `pin` has been gated in `self.parse_pin_and_mut()` so we don't need to gate it here.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// `pin [ const | mut ]`.
// `pin` has been gated in `self.parse_pin_and_mut()` so we don't need to gate it here.
// `pin [ const | mut ]`.
//
// `pin` has been gated in `self.parse_pin_and_mut()` so we don't
// need to gate it here.

The comment at the top of the function should also be adjusted to mention pin.

Comment on lines +2288 to +2291
(ast::Mutability::Not, ast::BorrowKind::Pin) => "&pin const ",
(ast::Mutability::Mut, ast::BorrowKind::Ref) => "&mut ",
(ast::Mutability::Mut, ast::BorrowKind::Raw) => "&raw mut ",
(ast::Mutability::Mut, ast::BorrowKind::Pin) => "&pin mut ",
Copy link
Contributor

Choose a reason for hiding this comment

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

"p" comes before "r", so let's reorder &pin lines before &raw ones.

Comment on lines +1 to +2
#![feature(pin_ergonomics)]
#![allow(dead_code, incomplete_features)]
Copy link
Contributor

Choose a reason for hiding this comment

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

This is preexisting, but it probably doesn't make a lot of sense that we put pin-ergonomics under the async-await directory. In some other PR, we might want to think about moving that.

Comment on lines +13 to +17
fn foo(_: Pin<&mut Foo>) {
}

fn foo_const(_: Pin<&Foo>) {
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
fn foo(_: Pin<&mut Foo>) {
}
fn foo_const(_: Pin<&Foo>) {
}
fn foo_pin_mut(_: Pin<&mut Foo>) {
}
fn foo_pin_reft(_: Pin<&Foo>) {
}

...to keep consistent with the names in the other test.

Comment on lines +6 to +7
// Makes sure we can handle `&pin mut place` and `&pin const place` as sugar for
// `unsafe { Pin::new_unchecked(&mut place) }` and `Pin::new(&place)`.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is wildly unsound, which gives me pause even about doing it on nightly, but the feature flag is marked as incomplete, and we're planning to follow-up with the borrow checker rules, so I suppose this is OK.

@traviscross
Copy link
Contributor

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Mar 9, 2025
@traviscross traviscross dismissed compiler-errors’s stale review March 9, 2025 09:29

The author addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants