Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions library/core/src/field.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,60 @@
//! Field Reflection
//! Field Reflection for field projections.
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.

Some more general information on FRTs (let me know if it makes sense to put this in the docs somewhere):

FRTs have two main properties that we need for field projections:

  1. They allow one to write code that is field-generic by just using plain generics.
  2. One can record information about fields that is then accessible in field-generic code.

The ultimate goal of field projections is to have an operator that allows one to go from Ptr<Struct> to Ptr<Field>. Since all customizable operators in Rust are backed by traits, we thought about how we could translate the syntactic information. We ended up by deducing that "generics are the natural way to write generic code in Rust, so let's just make fields be a special kind of generic". This explains why we need & how we arrived at the first property.

The second property is required to support Pin; we need some way to record the structural pinning information of fields that can be accessed from generic code. Our initial plan was to have a PinnableField trait that not only exposes a bool on the structuralness of its pinning, but also a generic associated type that is either the identity function, or the Pin type constructor (so type Bikeshed<P> = P; or type Bikeshed<P> = Pin<P>;). You can already implement this now as an unsafe extension trait of Field that is implemented through a safe derive macro. Since our original plan, we have put some more thought into this and the Move trait got some reviving traction. At the moment, we think the Move trait is a 10x better solution, so we have not really implemented or explored the PinnableField approach further.


The proposal has changed a fair bit; FRTs were designed and implemented in September last year and then reimplemented in January & then again in February. Their internal implementation changed a lot.

We're not sure if we need FRTs in the end. At the moment I'm leaning towards that we do not need them. However, they are still extremely useful for other purposes, so we might want to separate them into their own feature. For example, RfL wants them to better handle intrusive data structures: a struct can contain multiple instances of ListLinks, which allows it to be in multiple different lists at the same time. At the moment we use a const generic ID: u64 to record which ListLinks field a list uses; with FRTs, we could change that to a F: Field and then users would write List<field_of!(MyFieldType, foo_list)>, which would be much more meaningful and less error-prone.

For this reason, we might want to split it off as a standalone feature. It also explains the purpose of FRTs as being a tool of reflection a bit more, we want to be able to talk about properties of fields in a generic context. More on that in the thread about usage patterns in std. I'll give more information about the feature that supersedes FRTs in the offset_of! thread.

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.

This is useful background, thank you.

"generics are the natural way to write generic code in Rust, so let's just make fields be a special kind of generic"

My mental model is that we are very much not introducing a new kind of generic here (that would be impossible in library code), just a trait and some compiler-generated impls of it. Am I missing something, or does that sound accurate?

Pinnable fields

I think that all sounds pretty reasonable, though I think Pin is not the only structural thing... for example, the discussion we had around Option/Result mapping feels like it's ultimately the same kind of operation?

At the moment we use a const generic ID: u64 to record which ListLinks field a list uses; with FRTs, we could change that to a F: Field and then users would write List<field_of!(MyFieldType, foo_list)>, which would be much more meaningful and less error-prone.

I think this makes sense, but it (at least right now) also feels like it may not really belong in std -- or at least not be on a clear path to stabilization. Is it accurate that the primary benefit of this being in std is that we can get away without a derive-macro and needing to derive it on every type to synthesize the types/impls for 'field types'? That is definitely a large benefit for the users of the feature but it does seem like it fits under the reflection umbrella than projections.

//!
//! # Overview
//!
//! This module is part of the field projection compiler experiment; see the [tracking
//! issue](https://github.com/rust-lang/rust/issues/145383) for more information.
Comment on lines +5 to +6
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.

I also had another question: Have we accidentally added anything that is observable on stable? We still want to be able to change all aspects of FRTs (including removing them wholesale). To the best of my ability, we have not, but I'd like to get an experts' seal of approval :)

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 see anything here that looks exposed to stable. That question is often incorrectly answered, but I think you're operating in relatively safe territory right now. I'm much more worried about the other PR on projection traits, especially once/if they become something driving compiler behavior (rather than just being traits for library code).

//!
//! The current approach of field projections makes use of *field representing types*. These are
//! compiler-generated types that identify a field of a struct, union, enum or tuple. Users can
//! implement traits for them to store additional information about the *field*.
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 typically don't think of traits as storing information. Do you mean in the sense that the information is accessible via <FieldTy as Trait>::ASSOC_CONST or similar?

I guess in principle we could expose reflection-like information through the type (e.g., attributes on the field), but I presume that is not currently part of this proposal, right? And even if it was, not sure why we'd do that via traits vs. just having properties (fields or associated consts or methods) on the field type itself.

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.

Yes exactly, you use this information via the associated constants/types.

The reflection features of FRTs are needed because of Pin; we need a way to differentiate structurally pinned fields from normal fields. And since we want to represent subplaces with generic types (since we want to expose the type of the subplace), we naturally went into this direction. We could of course create field-generic operations by having fn op<const OFFSET: usize, T>() where T is the type of the field, but that now makes that operation unsafe, since you could pass any offset. So bundling the data in an unsafe trait was the fix for that. It also allows us to add more information to the trait in the future.

//!
//! # API Surface
//!
//! At its core, this module provides the [`field_of!`] macro, which takes a type and the name of a
//! field of that type and returns the field representing type (FRT) of that field.
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.

Do we support the same nested syntax that offset_of! does? I see that the macro definitions supports it, but the docs sort of imply it doesn't (https://doc.rust-lang.org/nightly/std/field/macro.field_of.html).

Is it accurate to say that in some sense, offset_of! should eventually be user-definable as field_of!(...).offset() or similar?

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, we do not support nested fields. To support enums, we allow field_of!(Enum, Variant.field), but you cannot do field_of!(Struct, nested.field). The reason for disallowing them is that if we did allow them, we would have to answer tough questions about orphan rules: is field_of!(LocalType, foreign_type.other_local_type) considered local, what about field_of!((ForeignType, LocalType), 1.field)?

offset_of! will thus not be definable through field_of!; but this is not the only reason, field_of! also does not record the offset for fields that have dynamic offset. So for example field_of!(Struct<dyn Trait>, tail) doesn't implement the Field trait (given that tail: dyn Trait), since dyn Trait has dynamic alignment and thus dynamic offset if not the first field. We still need to figure out more traits in that hierarchy, since the types of the base and the field are clearly something that should be available there.

The Projection (now named Subplace) trait from the design meeting takes on the role of handling dynamic projections. It allows nesting and much more than field_of!/offset_of!, we probably could expose a proj!($ident: $Type, $place_expr) macro that gives you the corresponding impl Subplace. That could then be used to implement offset_of!, since it also supports dynamic cases. However, the proj! macro itself is most likely very difficult to use correctly/safely, since a Subplace may carry special information for a specific instance of that subplace. So for example proj!(x: [u8], x[42]) can only be used for indexing into a slice with a length of at least 43. We're still figuring out where to do the range-check, if we move it into the actual place operation, then proj! will be fine, if we move it into the creation of Subplace, then an impl Subplace will be tied to a specific place and thus its usage is always unsafe.

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.

The reason for disallowing them is that if we did allow them, we would have to answer tough questions about orphan rules

Hm, this doesn't quite make sense to me. Isn't the expansion of a nested expression there 'just' querying the type of the field and then calling field_of! on that type? field_of! expands to a type name (roughly path_to_struct::Struct::field, if that actually existed)?

I am assuming that for the purposes of orphan rules the FRT is owned by the parent ADT crate and referenced conceptually from there with regards to the trait system, in all cases. Is that an incorrect assumption? That does imply that such types and impls on them are conceptually generated for all crates, even those that don't opt-in to the feature gate... but that seems fine?

dynamic projections

It feels a little odd to me to have two separate categories here. I would definitely expect to be able to get an FRT for tail: dyn Trait, though it seems quite reasonable for it to not have const OFFSET if that needs runtime information. Maybe what you want is a mirror of the Sized trait hierarchy (effectively an Offset hierarchy?) for the individual fields?

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.

Oh, actually, my mental model was wrong with expansions to a type name.

I'm wondering if there's a reason we want to define FieldRepresentingType here though? Maybe that could be a compiler-generated type for every field, which settles the orphan rules and whether users can accidentally create one not via the macro (no)?

That does mean there's not one FRT type but I'm not sure how much it buys us to have the name if you're always supposed to go via field_of! (I assume that's right?)

//!
//! The FRTs of certain well-behaved (at the moment `repr(packed)` and `?Sized` are not supported)
//! fields implement the [`Field`] trait, which exposes information such as the offset, base type,
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.

Suggested change
//! fields implement the [`Field`] trait, which exposes information such as the offset, base type,
//! types implement the [`Field`] trait, which exposes information such as the offset, base type,

Right?

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, fields is correct, the sentence without the parenthesis says: The FRTs of certain well-behaved fields implement the [`Field`] trait. So the FRT implements the Field trait if the field is well-behaved. Is there a better way to phrase this? Maybe the parenthesis is preventing from easily parsing the sentence, it could be a footnote instead.

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.

Are you trying to say that Field is not implemented if the field is repr(packed) or if the parent type is repr(packed)? That's the confusion I was trying to clear up here.

(I think repr(packed) influencing this is a bit confusing. That shouldn't prevent offsets being statically known, right?)

//! and type of the field.
//!
//! # Implementation Details
//!
//! FRTs are a mix of compiler and library implemented. The perma-unstable `FieldRepresentingType`
//! struct is used to ultimately represent a field by the variant and field index; while the
//! compiler performs existence and coherence checks before allowing access to it.
//!
//! FRTs are intended to contain only *static* information about a field and as such they are
//! inhabited ZSTs. For this reason, they also implement [`Copy`], [`Send`], and [`Sync`] regardless
//! of the properties of the base type.

use crate::marker::PhantomData;

/// Field Representing Type
/// A type representing a field of a struct, union, enum or tuple.
///
/// This perma-unstable type is part of the implementation details of the [`field_of!`] macro. This
/// type is only instantiated by the compiler when the type `T` has a variant with the index
/// `VARIANT` and that variant has a field with index `FIELD`. This type will then represent that
/// specific field.
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.

Does it matter when the compiler chooses to instantiate this? Can't user code choose to instantiate it at different types/constants, e.g., with FieldRepresentingType::<Vec<u32>, 0, 1>?

Will that be a post-mono error if Vec turns out to not have that field/variant?

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 way it is currently implemented, yes, this matters a lot. If you write FieldRepresentingType::<Vec<u32>, 0, 42>, then that type exists (so no error), but does not implement the Field trait. If you try to use that type in a context that expects it to implement the Field trait, the compiler will ICE, since it just uses the variant/field index to index into the ADT data :)

I didn't see the need to make this more accessible, since this developed much later during implementation to avoid having to add another TyKind and we intend it to remain perma-unstable. If you think it's a useful thing to have or you want to make it more robust, then I'm not opposed to changing it.

///
/// Since this type is marked as `#[fundamental]`, downstream crates may implement types as long as
/// `T` is local to that crate.
#[unstable(feature = "field_representing_type_raw", issue = "none")]
#[lang = "field_representing_type"]
#[expect(missing_debug_implementations)]
#[fundamental]
#[doc(hidden)]
pub struct FieldRepresentingType<T: ?Sized, const VARIANT: u32, const FIELD: u32> {
_phantom: PhantomData<T>,
}

// SAFETY: `FieldRepresentingType` doesn't contain any `T`
// SAFETY: `FieldRepresentingType` doesn't contain any `T`.
Comment thread
BennoLossin marked this conversation as resolved.
unsafe impl<T: ?Sized, const VARIANT: u32, const FIELD: u32> Send
for FieldRepresentingType<T, VARIANT, FIELD>
{
}

// SAFETY: `FieldRepresentingType` doesn't contain any `T`
// SAFETY: `FieldRepresentingType` doesn't contain any `T`.
unsafe impl<T: ?Sized, const VARIANT: u32, const FIELD: u32> Sync
for FieldRepresentingType<T, VARIANT, FIELD>
{
Expand All @@ -36,7 +73,7 @@ impl<T: ?Sized, const VARIANT: u32, const FIELD: u32> Clone
}
}
Comment thread
BennoLossin marked this conversation as resolved.

/// Expands to the field representing type of the given field.
/// The field representing type of the given field.
///
/// The container type may be a tuple, `struct`, `union` or `enum`. In the case of an enum, the
/// variant must also be specified. Only a single field is supported.
Expand Down
Loading