Skip to content

add lang items for field projections via virtual places#154940

Open
BennoLossin wants to merge 20 commits intorust-lang:mainfrom
BennoLossin:field-projections-lang-items
Open

add lang items for field projections via virtual places#154940
BennoLossin wants to merge 20 commits intorust-lang:mainfrom
BennoLossin:field-projections-lang-items

Conversation

@BennoLossin
Copy link
Copy Markdown
Contributor

@BennoLossin BennoLossin commented Apr 7, 2026

View all comments

@oli-obk I'll be assigning Mark to review this, since this is exclusively library changes.

r? @Mark-Simulacrum

  • Still need to improve the docs, but I figured I'd just let people comment on this, because of my curse of knowledge.
  • I felt the need to have some module-level documentation for these operators, but I'm not sure if that would be good to put in the general ops module, maybe we should have a ops::place submodule and put all the things there?
  • The safety docs are something that I'd leave for later, when we have a formal model and have spent some time figuring them out. I'd just add as section to the module-level description talking about safety and link it from each trait for now.

@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. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Apr 7, 2026
@rust-log-analyzer

This comment has been minimized.

@BennoLossin BennoLossin force-pushed the field-projections-lang-items branch from e948f37 to cb85148 Compare April 7, 2026 09:52
@rust-log-analyzer

This comment has been minimized.

@BennoLossin BennoLossin force-pushed the field-projections-lang-items branch from cb85148 to 5433734 Compare April 7, 2026 12:06
@BennoLossin
Copy link
Copy Markdown
Contributor Author

@rustbot label +F-field_projections

@rustbot rustbot added the F-field_projections `#![feature(field_projections)]` label Apr 7, 2026
@BennoLossin BennoLossin force-pushed the field-projections-lang-items branch 2 times, most recently from 0f63962 to 1fadbb7 Compare April 8, 2026 09:39
Copy link
Copy Markdown
Member

@Mark-Simulacrum Mark-Simulacrum left a comment

Choose a reason for hiding this comment

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

Some initial comments... I think similar to the other PR it would help to have some docs guiding review of impls, especially in the absence of Safety docs. Are there any stable-visible implications from such impls (today or intended) even while the traits remain unstable?

Also happy to schedule time to talk about it if that would help.

View changes since this review

/// When the compiler emits the call to `read`, the borrow checker guarantees that no other
/// operation is conflicting with this one.
#[lang = "place_read_safety"]
const SAFETY: bool;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this interacts with the as-yet unknown Safety comments, but I think we'll want to think about how to define an explanation for what this actually controls. (And in the meantime, I'd love guidance on how to review what value is set here for impls).

If I'm following correctly, we expect that there are actually really two classes of PlaceRead implementations: ones which only need compiler-provided guarantees to call read(), for which SAFETY = true, and a separate class for which the compiler-provided guarantees are insufficient (SAFETY = false).

And the reason we can't used RFC 3245 (refined impls) to meet this is because read() is always itself unsafe to call due to the raw pointer.

If that's accurate, I'm not sure what the second paragraph ("When the compiler emits the call to read, the borrow checker guarantees that no other operation is conflicting with this one.") means in relation to this constant. Are we actually saying that SAFETY = false means borrow checker would not guarantee that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Your characterization is spot-on. I no longer follow my own reasoning for having that second paragraph 😅. I think what I wanted to say with that is "if this is set to true, then the safety requirements of the operation are exactly those guaranteed by the compiler when borrow-check succeeds for a program".

Is this better?

    /// When the operator is used, the borrow checker follows its usual rules to ensure that no
    /// other operation conflicts with this one. If that alone is sufficient to make this operation
    /// sound, then this should be `true`.

I still feel like it's a bit cryptic.

As for guiding impls in std, this value should most of the time be true, so for all smart pointers like Box, Arc &mut T etc. Only pointers that don't guarantee that they are always valid will have false here like *mut T, NonNull<T> etc.

/// When the compiler emits the call to `read`, the borrow checker guarantees that no other
/// operation is conflicting with this one.
#[lang = "place_read_safety"]
const SAFETY: bool;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Separately from the other thread, probably not for this PR, it is not clear to me how this can be an associated constant. We want safety to be defined at ~HIR level (at least roughly), I think, and so we don't yet actually know complex constants there. For implementations in the standard library (at least) should we have a restriction that this is always a trivial boolean (false/true) rather than some more complex expression?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Our current implementation plan is to only allow true or false here. Eventually, we could relax this to allow for expressions that always evaluate to a concrete value when encountering a use of the operator.


/// Reading a place `let val = *x;`.
///
/// When `x: Self`, then `let val = *x;` will be desugared into [`PlaceRead::read`].
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't quite see how to work from that expression to the trait invocation. It seems like we need to know S to know which <Self as PlaceRead<S>>::place_read(x, sub) to call, but where is S coming from? Are we expecting to infer S from a unique impl of Subplace::Target for an inferred typeof(val)? That feels somewhat unlikely. (This S point is common across a bunch of the traits here).

Can we perhaps show the intended desugaring here in full?

IIUC, replacing *x with such a desugaring would also generally not be equivalent because that would mean that borrow check stops checking the expression correctly, right? Do we expect that to change at some future date, where borrow check actually always acts on ::place_read calls and there is no 'read' primitive other than that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The S is derived from the place expression the operation is applied to. We have a rough sketch of the algorithm, but in this simple example, the desugaring looks like this:

// Let's say the target of `X` is `Y` and that it has a blanket impl of
// `PlaceRead` for all subplaces.
let x: X = ...;

let val = *x;
// desugars to:
let val = unsafe {
    <P as PlaceRead<FullSubplace<Y>>>::read(&raw const x)
};
// With `FullSubplace` defined as a ZST and this impl:
impl<T: ?Sized> Subplace for FullSubplace<T> {
    type Source = T;
    type Target = T;

    fn offset(
        self,
        meta: <T as Pointee>::Metadata,
    ) -> (usize, <T as Pointee>::Metadata) {
        (0, meta)
    }
}

I don't think it's a good idea to plaster this on every operation trait, we should link to a section in the docs that explains this once. Additionally, this is independent of each operation trait, as this desugaring is attached to the place expression the operation is done on.

My current plan is to implement a set of macros that do the desugaring in the compiler as the next step after the lang items. We already have a rough sketch of the desugaring algorithm. But I'd like to implement it in code before committing it to documentation properly. Would it be okay with you if I added a stub section in the module-level docs of the ops::place submodule that I link to from the traits regarding desugaring? I would fill the stub in the next PR along with the desugaring macros.

Regarding the second point: We will most likely not change the borrow checker to act on PlaceRead::read calls; instead we will likely just change MIR to have a custom read, or change the existing read operation to allow for custom code doing the actual read. So in that sense, the operator isn't actually a desugared before MIR. But I think it's good to teach it that way, since Rust users don't really know about MIR all that much.

/// The type of the contained place.
#[lang = "place_target"]
type Target: ?Sized;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Am I interpreting right that this will commonly be implemented for a "Field Representing Type"? Or does this actually not have much relation to that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, this trait will be implemented for (dumb & smart) pointers like Box, Arc, *mut, &mut, NonNull etc. It will also be implemented for place wrapper like MaybeUninit, Cell etc. Anything that conceptually "points at a place" or "contains a place" should implement this trait. Maybe the name Place is a bit misleading after all, especially because Subplace is representing an actual subplace... I'm discussing the name bikeshedding more on zulip.

Comment on lines +85 to +87
/// When `x: Self` and one performs a [`PlaceRead::read`] where the target value is not [`Copy`],
/// then the compiler checks if this trait is implemented and if so, moves the value out by reading
/// it and adjusting the borrow checker state of the place.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it accurate to say that if this trait is not implemented, then moving out of a place is denied by the compiler? i.e., whenever we have let val = *p; we are injecting assert_place_move::<Self>()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct. But we're only performing the check typeof(p): PlaceMove if the pointee (S::Target) doesn't implement Copy. So we would for example not implement PlaceMove for &T, but you could still "move out" of a &u8, since u8: Copy.

@@ -0,0 +1,220 @@
use crate::field::Subplace;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be good to have a module doc -- probably drawing on the recent lang design doc -- that explains Place, Subplace, and the mental model that the individual traits are operations on places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That was also my impression. Any ideas for the name of the module? ops::place sounds like a very sensible idea. Alternatively, having it in core::place might also work, but then we're no longer grouping all operators under core::ops. What do you think?

/// Dropping a place.
///
/// Emitted by the compiler when a place has been partially moved out and the pointer with ownership
/// is being dropped.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd love an example desugaring here of the time at which drop(...) gets called and the time at which drop_husk(...) would later(?) get called.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure!

struct Greeter {
    greeting: String,
    greetee: String,
}

unsafe impl<S> PlaceDrop<S> for Box<S::Source>
where
    S: Subplace,
{
    /* ... */
}

unsafe impl<T: ?Sized> DropHusk for Box<T> { /* this is the normal drop impl of `Box`! */ }

fn main() {
    let mut greeter = Box::new(Greeter {
        greeting: String::from("Hello"),
        greetee: String::from("world"),
    });
    
    let _greeting: String = greeter.greeting;
    // now the place `*greeter` is partially moved out.

    // now `greeter` is dropped here & but before all active subplaces are dropped.
}

// also with "normal" drop-elaboration (I'm just using `drop` for this)
fn desugared_main() {
    let mut greeter = Box::new(Greeter {
        greeting: String::from("Hello"),
        greetee: String::from("world"),
    });
    
    let _greeting: String = unsafe { 
        <Box<Greeter> as PlaceRead<proj!(greeter.greeting)>>::read(&raw const greeter)
    };

    drop(_greeting);

    unsafe {
        <Box<Greeter> as PlaceDrop<proj!(greeter.greetee)>>::drop(&raw const greeter);
    }

    unsafe { <Box<Greeter> as DropHusk>::drop_husk(&raw const greeter) };
}

I decided to also create a more complex example with dropflags and multiple fields:

Details
struct Greeter {
    greeting: String,
    greetee: Greetee,
    end: String,
}

struct Greetee {
    title: Option<String>,
    greetee: String,
}

unsafe impl<S> PlaceDrop<S> for Box<S::Source>
where
    S: Subplace,
{
    /* ... */
}

unsafe impl<T: ?Sized> DropHusk for Box<T> { /* this is the normal drop impl of `Box`! */ }

fn main() {
    let mut greeter = Box::new(Greeter {
        greeting: String::from("Hello"),
        greetee: Greetee {
            title: Some(String::from("awesome")),
            greetee: String::from("world"),
        },
        end: String::from("!"),
    });
    
    let _greeting: String = greeter.greeting;
    // now the place `*greeter` is partially moved out.

    // To make things more interesting, let's conditionally move out of a sub-sub-place 
    if random() {
        let _title: String = greeter.greetee.title;
    }
    // now `greeter` is dropped here & but before all active subplaces are dropped.
}

// also with "normal" drop-elaboration (I'm just using `drop` for this)
fn desugared_main() {
    let mut greeter = Box::new(Greeter {
        greeting: String::from("Hello"),
        greetee: Greetee {
            title: Some(String::from("awesome")),
            greetee: String::from("world"),
        },
        end: String::from("!"),
    });
    
    let _greeting: String = unsafe { 
        <Box<Greeter> as PlaceRead<proj!(greeter.greeting)>>::read(&raw const greeter)
    };

    let mut dropflag#title = true;

    if random() {
        let _title: String = unsafe { 
            <Box<Greeter> as PlaceRead<proj!(greeter.greetee.title)>>::read(&raw const greeter)
        };
        dropflag#title = false;
        drop(_title);
    }

    drop(_greeting);

    if dropflag#title {
        // we can drop the whole thing, since if we didn't hit `random()`, then
        // `greetee` is still initialized.
        unsafe {
            <Box<Greeter> as PlaceDrop<proj!(greeter.greetee)>>::drop(&raw const greeter);
        }
    } else {
        unsafe {
            <Box<Greeter> as PlaceDrop<proj!(greeter.greetee.greetee)>>::drop(&raw const greeter);
        }
    }

    unsafe {
        <Box<Greeter> as PlaceDrop<proj!(greeter.end)>>::drop(&raw const greeter);
    }

    unsafe { <Box<Greeter> as DropHusk>::drop_husk(&raw const greeter) };
}

Comment thread library/core/src/field.rs Outdated
/// - field accesses,
/// - array/slice indexes.
///
/// This represents an arbitrary chaining of these; it can also be empty.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"emptiness" here is not really something the user can observe, at least without additional knowledge, right? Because Source == Target is not enough for ?Sized types (for Sized it is I think enough).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That is correct. I added this part to make people aware of the fact that a subplace doesn't have to be a strict subplace, but can also just be the entire original place.

This comes up a lot in practice, for example writing let x = @*y; will invoke PlaceBorrow with FullSubplace.

Comment thread library/core/src/field.rs Outdated
fn offset(
self,
metadata: <Self::Source as Pointee>::Metadata,
) -> (usize, <Self::Target as Pointee>::Metadata);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it be accurate to think of this as being implemented for (impl Field, impl Field, impl Field, ...)? I think the answer is 'not quite' because array indexing is not Field'able, since FRTs are always ZSTs? But maybe Field should be generalized such that they can contain arbitrary metadata (I guess that's what Subplace is)?

Semi-related, how does this work if I have foo[idx] where idx: !Copy? That seems to rule out desugaring to Subplace, since Subplace: Copy. Do we expect that desugaring? Is the intent that array indexing doesn't actually go through place-traits?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kinda yes, but FRTs can only contain static information, so unsized fields cannot be represented by them. Then as you already mentioned array and slice indexing is also dynamic, so we need primitive subplaces for them. Yes again to the statement that Subplace is a generalization of Field. Two more notes on this: 1) this is the reason why I think that we don't need FRTs for field projection at the moment, we have a more general feature that we already need. 2) the compiler-generated types that implement Subplace will not be #[fundamental] because of the orphan rule issues I mentioned in the other PR. So one cannot store special information on subplaces like one is able to with FRTs on fields.

I imagine to just have this lang item similar to FRTs that will be the actual type used behind the scenes:

pub struct SubplaceChain<S1, S2>(S1, S2);

unsafe impl<S1, S2> Subplace for SubplaceChain<S1, S2>
where
    S1: Subplace,
    S2: Subplace<Source = S1::Target>,
{
    type Source = S1::Source;
    type Target = S2::Target;

    fn offset(self, meta: <S1::Source as Pointee>::Metadata) -> (usize, <S2::Target as Pointee>::Metadata) {
        let (off1, meta) = self.0.offset(meta);
        let (off2, meta) = self.1.offset(meta);
        (off1 + off2, meta)
    }
}

We then also have one for the FullSubplace as I gave in the other thread. We then probably also want some for array and slice indexing and then finally implement Subplace for FRTs.

Regarding !Copy indexes: that's a great observation! We need to lift that restriction then. While we don't support custom indexing at the moment, we want to keep the door open for experimentation later.

- _0 = offset::<*const i32, isize>(move _3, move _4) -> [return: bb1, unwind unreachable];
- _0 = std::intrinsics::offset::<*const i32, isize>(move _3, move _4) -> [return: bb1, unwind unreachable];
+ _0 = Offset(move _3, move _4);
+ goto -> bb1;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What happened here? It doesn't seem like we're changing anything in MIR building...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change also confused me a lot, but I needed it to pass CI. I asked @dingxiangfei2009 about it and he said that it is due to the potential ambiguity the Subplace::offset function introduces. I don't know how the name selection algorithm in MIR works, but Xiang said that he would like it if MIR always used the fully qualified name. But that should be a different PR. Not sure if we can do anything here (IIUC).

@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 Apr 11, 2026
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 11, 2026

Reminder, once the PR becomes ready for a review, use @rustbot ready.

Copy link
Copy Markdown
Contributor Author

@BennoLossin BennoLossin left a comment

Choose a reason for hiding this comment

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

Thanks a lot for taking a look!

We do not intend any stable-visible implications with these traits. AFAIK, we don't have any today, but I'm not too confident in that statement, as I am not familiar with what things to watch out for. Would be great to get your assessment!

I'd be happy to have a meeting!

View changes since this review

@@ -0,0 +1,220 @@
use crate::field::Subplace;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That was also my impression. Any ideas for the name of the module? ops::place sounds like a very sensible idea. Alternatively, having it in core::place might also work, but then we're no longer grouping all operators under core::ops. What do you think?

/// The type of the contained place.
#[lang = "place_target"]
type Target: ?Sized;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, this trait will be implemented for (dumb & smart) pointers like Box, Arc, *mut, &mut, NonNull etc. It will also be implemented for place wrapper like MaybeUninit, Cell etc. Anything that conceptually "points at a place" or "contains a place" should implement this trait. Maybe the name Place is a bit misleading after all, especially because Subplace is representing an actual subplace... I'm discussing the name bikeshedding more on zulip.


/// Reading a place `let val = *x;`.
///
/// When `x: Self`, then `let val = *x;` will be desugared into [`PlaceRead::read`].
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The S is derived from the place expression the operation is applied to. We have a rough sketch of the algorithm, but in this simple example, the desugaring looks like this:

// Let's say the target of `X` is `Y` and that it has a blanket impl of
// `PlaceRead` for all subplaces.
let x: X = ...;

let val = *x;
// desugars to:
let val = unsafe {
    <P as PlaceRead<FullSubplace<Y>>>::read(&raw const x)
};
// With `FullSubplace` defined as a ZST and this impl:
impl<T: ?Sized> Subplace for FullSubplace<T> {
    type Source = T;
    type Target = T;

    fn offset(
        self,
        meta: <T as Pointee>::Metadata,
    ) -> (usize, <T as Pointee>::Metadata) {
        (0, meta)
    }
}

I don't think it's a good idea to plaster this on every operation trait, we should link to a section in the docs that explains this once. Additionally, this is independent of each operation trait, as this desugaring is attached to the place expression the operation is done on.

My current plan is to implement a set of macros that do the desugaring in the compiler as the next step after the lang items. We already have a rough sketch of the desugaring algorithm. But I'd like to implement it in code before committing it to documentation properly. Would it be okay with you if I added a stub section in the module-level docs of the ops::place submodule that I link to from the traits regarding desugaring? I would fill the stub in the next PR along with the desugaring macros.

Regarding the second point: We will most likely not change the borrow checker to act on PlaceRead::read calls; instead we will likely just change MIR to have a custom read, or change the existing read operation to allow for custom code doing the actual read. So in that sense, the operator isn't actually a desugared before MIR. But I think it's good to teach it that way, since Rust users don't really know about MIR all that much.

/// When the compiler emits the call to `read`, the borrow checker guarantees that no other
/// operation is conflicting with this one.
#[lang = "place_read_safety"]
const SAFETY: bool;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Your characterization is spot-on. I no longer follow my own reasoning for having that second paragraph 😅. I think what I wanted to say with that is "if this is set to true, then the safety requirements of the operation are exactly those guaranteed by the compiler when borrow-check succeeds for a program".

Is this better?

    /// When the operator is used, the borrow checker follows its usual rules to ensure that no
    /// other operation conflicts with this one. If that alone is sufficient to make this operation
    /// sound, then this should be `true`.

I still feel like it's a bit cryptic.

As for guiding impls in std, this value should most of the time be true, so for all smart pointers like Box, Arc &mut T etc. Only pointers that don't guarantee that they are always valid will have false here like *mut T, NonNull<T> etc.

/// When the compiler emits the call to `read`, the borrow checker guarantees that no other
/// operation is conflicting with this one.
#[lang = "place_read_safety"]
const SAFETY: bool;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Our current implementation plan is to only allow true or false here. Eventually, we could relax this to allow for expressions that always evaluate to a concrete value when encountering a use of the operator.

Comment on lines +85 to +87
/// When `x: Self` and one performs a [`PlaceRead::read`] where the target value is not [`Copy`],
/// then the compiler checks if this trait is implemented and if so, moves the value out by reading
/// it and adjusting the borrow checker state of the place.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct. But we're only performing the check typeof(p): PlaceMove if the pointee (S::Target) doesn't implement Copy. So we would for example not implement PlaceMove for &T, but you could still "move out" of a &u8, since u8: Copy.

/// Dropping a place.
///
/// Emitted by the compiler when a place has been partially moved out and the pointer with ownership
/// is being dropped.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure!

struct Greeter {
    greeting: String,
    greetee: String,
}

unsafe impl<S> PlaceDrop<S> for Box<S::Source>
where
    S: Subplace,
{
    /* ... */
}

unsafe impl<T: ?Sized> DropHusk for Box<T> { /* this is the normal drop impl of `Box`! */ }

fn main() {
    let mut greeter = Box::new(Greeter {
        greeting: String::from("Hello"),
        greetee: String::from("world"),
    });
    
    let _greeting: String = greeter.greeting;
    // now the place `*greeter` is partially moved out.

    // now `greeter` is dropped here & but before all active subplaces are dropped.
}

// also with "normal" drop-elaboration (I'm just using `drop` for this)
fn desugared_main() {
    let mut greeter = Box::new(Greeter {
        greeting: String::from("Hello"),
        greetee: String::from("world"),
    });
    
    let _greeting: String = unsafe { 
        <Box<Greeter> as PlaceRead<proj!(greeter.greeting)>>::read(&raw const greeter)
    };

    drop(_greeting);

    unsafe {
        <Box<Greeter> as PlaceDrop<proj!(greeter.greetee)>>::drop(&raw const greeter);
    }

    unsafe { <Box<Greeter> as DropHusk>::drop_husk(&raw const greeter) };
}

I decided to also create a more complex example with dropflags and multiple fields:

Details
struct Greeter {
    greeting: String,
    greetee: Greetee,
    end: String,
}

struct Greetee {
    title: Option<String>,
    greetee: String,
}

unsafe impl<S> PlaceDrop<S> for Box<S::Source>
where
    S: Subplace,
{
    /* ... */
}

unsafe impl<T: ?Sized> DropHusk for Box<T> { /* this is the normal drop impl of `Box`! */ }

fn main() {
    let mut greeter = Box::new(Greeter {
        greeting: String::from("Hello"),
        greetee: Greetee {
            title: Some(String::from("awesome")),
            greetee: String::from("world"),
        },
        end: String::from("!"),
    });
    
    let _greeting: String = greeter.greeting;
    // now the place `*greeter` is partially moved out.

    // To make things more interesting, let's conditionally move out of a sub-sub-place 
    if random() {
        let _title: String = greeter.greetee.title;
    }
    // now `greeter` is dropped here & but before all active subplaces are dropped.
}

// also with "normal" drop-elaboration (I'm just using `drop` for this)
fn desugared_main() {
    let mut greeter = Box::new(Greeter {
        greeting: String::from("Hello"),
        greetee: Greetee {
            title: Some(String::from("awesome")),
            greetee: String::from("world"),
        },
        end: String::from("!"),
    });
    
    let _greeting: String = unsafe { 
        <Box<Greeter> as PlaceRead<proj!(greeter.greeting)>>::read(&raw const greeter)
    };

    let mut dropflag#title = true;

    if random() {
        let _title: String = unsafe { 
            <Box<Greeter> as PlaceRead<proj!(greeter.greetee.title)>>::read(&raw const greeter)
        };
        dropflag#title = false;
        drop(_title);
    }

    drop(_greeting);

    if dropflag#title {
        // we can drop the whole thing, since if we didn't hit `random()`, then
        // `greetee` is still initialized.
        unsafe {
            <Box<Greeter> as PlaceDrop<proj!(greeter.greetee)>>::drop(&raw const greeter);
        }
    } else {
        unsafe {
            <Box<Greeter> as PlaceDrop<proj!(greeter.greetee.greetee)>>::drop(&raw const greeter);
        }
    }

    unsafe {
        <Box<Greeter> as PlaceDrop<proj!(greeter.end)>>::drop(&raw const greeter);
    }

    unsafe { <Box<Greeter> as DropHusk>::drop_husk(&raw const greeter) };
}

Comment thread library/core/src/field.rs Outdated
/// - field accesses,
/// - array/slice indexes.
///
/// This represents an arbitrary chaining of these; it can also be empty.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That is correct. I added this part to make people aware of the fact that a subplace doesn't have to be a strict subplace, but can also just be the entire original place.

This comes up a lot in practice, for example writing let x = @*y; will invoke PlaceBorrow with FullSubplace.

Comment thread library/core/src/field.rs Outdated
fn offset(
self,
metadata: <Self::Source as Pointee>::Metadata,
) -> (usize, <Self::Target as Pointee>::Metadata);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kinda yes, but FRTs can only contain static information, so unsized fields cannot be represented by them. Then as you already mentioned array and slice indexing is also dynamic, so we need primitive subplaces for them. Yes again to the statement that Subplace is a generalization of Field. Two more notes on this: 1) this is the reason why I think that we don't need FRTs for field projection at the moment, we have a more general feature that we already need. 2) the compiler-generated types that implement Subplace will not be #[fundamental] because of the orphan rule issues I mentioned in the other PR. So one cannot store special information on subplaces like one is able to with FRTs on fields.

I imagine to just have this lang item similar to FRTs that will be the actual type used behind the scenes:

pub struct SubplaceChain<S1, S2>(S1, S2);

unsafe impl<S1, S2> Subplace for SubplaceChain<S1, S2>
where
    S1: Subplace,
    S2: Subplace<Source = S1::Target>,
{
    type Source = S1::Source;
    type Target = S2::Target;

    fn offset(self, meta: <S1::Source as Pointee>::Metadata) -> (usize, <S2::Target as Pointee>::Metadata) {
        let (off1, meta) = self.0.offset(meta);
        let (off2, meta) = self.1.offset(meta);
        (off1 + off2, meta)
    }
}

We then also have one for the FullSubplace as I gave in the other thread. We then probably also want some for array and slice indexing and then finally implement Subplace for FRTs.

Regarding !Copy indexes: that's a great observation! We need to lift that restriction then. While we don't support custom indexing at the moment, we want to keep the door open for experimentation later.

- _0 = offset::<*const i32, isize>(move _3, move _4) -> [return: bb1, unwind unreachable];
- _0 = std::intrinsics::offset::<*const i32, isize>(move _3, move _4) -> [return: bb1, unwind unreachable];
+ _0 = Offset(move _3, move _4);
+ goto -> bb1;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change also confused me a lot, but I needed it to pass CI. I asked @dingxiangfei2009 about it and he said that it is due to the potential ambiguity the Subplace::offset function introduces. I don't know how the name selection algorithm in MIR works, but Xiang said that he would like it if MIR always used the fully qualified name. But that should be a different PR. Not sure if we can do anything here (IIUC).

@rust-bors

This comment has been minimized.

Comment thread library/core/src/field.rs Outdated
#[rustc_deny_explicit_impl]
#[rustc_dyn_incompatible_trait]
#[lang = "subplace"]
pub unsafe trait Subplace: Sized + Copy {
Copy link
Copy Markdown
Contributor Author

@BennoLossin BennoLossin Apr 16, 2026

Choose a reason for hiding this comment

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

I put this trait here in field.rs, since I didn't have a better module in mind. It isn't an operator trait, so ops felt very wrong. Now that I'm adding the ops::place module, I think it could be fine to add it there, we would break the rule of "only ops in ops", but it's a submodule and the Subplace trait really only is interesting there.

Another place where it could go is core::marker.

Thoughts?

View changes since the review

@BennoLossin BennoLossin force-pushed the field-projections-lang-items branch from 6e48289 to f5cbf5d Compare April 16, 2026 06:09
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 16, 2026

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@BennoLossin
Copy link
Copy Markdown
Contributor Author

Arg, I accidentally pushed some local changes that I wanted to put on top of the rebase... Here is the actual range-diff

@rust-log-analyzer

This comment has been minimized.

@BennoLossin
Copy link
Copy Markdown
Contributor Author

@rustbot ready

@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 Apr 21, 2026
Comment thread library/core/src/ops/place.rs Outdated
Comment thread library/core/src/ops/place.rs Outdated
Comment thread library/core/src/ops/place.rs

use crate::ptr::Pointee;

/// A subplace of [`Self::Source`] with the type [`Self::Target`].
Copy link
Copy Markdown
Member

@tmandry tmandry Apr 21, 2026

Choose a reason for hiding this comment

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

Is it true that types implementing this trait are always created by the compiler? If so it would help to clarify that. Otherwise, I'd add that the compiler does create some special types that implement this trait.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm I don't think we have that figured out yet. I think that we could get away with just having some lang items that implement this trait & no user-impls. But we want to keep it open for custom subplaces (for example the one of HashMap).

Comment thread library/core/src/ops/place.rs Outdated

/// Forwards the subplace `S` of the place contained by this.
///
/// When `x: Self` and `Self::Target` has a subplace `S` accessible via
Copy link
Copy Markdown
Member

@tmandry tmandry Apr 21, 2026

Choose a reason for hiding this comment

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

One thing I'm unclear about: How does the Wrapped type get created?

Below there's a test that uses an actual wrapper type implementing Subplace, but I thought that was not going to be allowed (and the test includes errors for that).

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is essentially the same answer as the other thread: #154940 (comment). i.e. we either allow implementing it, or have a few lang items that users would use instead.

(I created the test for the feature gate errors of implementing it, we should probably have it regardless of being allowed to implement it or not)

@BennoLossin
Copy link
Copy Markdown
Contributor Author

I just pushed the hopefully final trait rename of this PR. I'm very happy with the current names & the story they are telling. They are of course temporary until we accept an RFC or until we stabilize the traits.

Let me know if I should squash the commits, I didn't force push to help with commit-based reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

F-field_projections `#![feature(field_projections)]` 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. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants