core: Rewrite docs for try_as_dyn - #162312
Merged
Merged
Conversation
Collaborator
|
r? @JohnTitor rustbot has assigned @JohnTitor. Use Why was this reviewer chosen?The reviewer was selected based on:
|
This comment has been minimized.
This comment has been minimized.
jnkel
force-pushed
the
try_as_dyn-docs
branch
2 times, most recently
from
September 5, 2026 06:24
8626105 to
793a9ea
Compare
oli-obk
reviewed
Sep 14, 2026
Contributor
|
@bors squash msg="core: Rewrite docs for try_as_dyn" |
This comment has been minimized.
This comment has been minimized.
Contributor
|
🔨 2 commits were squashed into ed74429. |
rust-bors
Bot
force-pushed
the
try_as_dyn-docs
branch
from
September 14, 2026 16:40
1c71f1f to
ed74429
Compare
Contributor
|
hm damn, that made it coauthored by me :( I was hoping it would just be your commit. Oh well. lgtm, you may want to force push the commit so it is just yours |
jnkel
force-pushed
the
try_as_dyn-docs
branch
from
September 14, 2026 17:39
ed74429 to
58c9d2f
Compare
Contributor
|
@bors r+ rollup |
Contributor
JonathanBrouwer
added a commit
to JonathanBrouwer/rust
that referenced
this pull request
Sep 17, 2026
core: Rewrite docs for try_as_dyn Update the documentation for `try_as_dyn` to resolve confusion some people had about the feature, provide better examples, and promise less. In particular, this PR updates the documentation to never promise success, allowing for spurious failures for even "simple" cases. This reflects the general consensus in https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/try_as_dyn.20potential.20problematic.20implications/near/621331808, since the precise rules of the implementation can be quite subtle and difficult to explain to users. Instead, document the function as being best-effort and able to produce false negatives, with non-exhaustive examples of situations where it produces false negatives in practice. We can always make the guarantees stricter in the future. Also replaces the "animal-cat-dog" style example with more realistic use cases of `try_as_dyn` for performance and debugging. cc @oli-obk <details><summary>Rendered</summary> Returns `Some(&U)` if `T` can be coerced to the dyn trait type `U`. Otherwise, it returns `None`. > **Warning** > > This function is implemented on a best-effort basis. It is not always possible to determine whether a generic type implements a trait; thus, this function may produce false negatives, returning `None` even when `T` implements the requested trait. > > `try_as_dyn` is guaranteed to return `None` if `T` does *not* implement the requested trait, but it is never guaranteed to return `Some`. It is intended to be used for performance optimizations and debugging, and `try_as_dyn` succeeding for a particular type should never be relied upon for correctness (i.e. callers must behave correctly even if `try_as_dyn` spuriously returns `None`). </div> # Examples of false negatives Some examples of situations where `try_as_dyn::<T, dyn Trait>` returns `None` in practice even when `T` implements `Trait`: * `T`'s impl for `Trait` is lifetime-dependent * `T`'s impl for `Trait` is a builtin impl (e.g. `dyn Debug` implements `Debug`) * `T`'s impl for `Trait` has a trait bound which requires transitively reasoning about lifetime-dependent or builtin impls This list is not exhaustive. There is some detailed documentation about these limitations at <https://doc.rust-lang.org/unstable-book/library-features/try-as-dyn.html> But the gist is summarized below: ## Lifetime-dependent impls `try_as_dyn` does not have access to lifetime information, thus it cannot differentiate between `'static` and other lifetimes and cannot reason about outlives bounds on impls. Thus it cannot reason about impls that have `'static` lifetimes or outlives bounds of any kind. /// The following impls are lifetime-dependent and produce false negatives when used with `try_as_dyn`: ```rust # trait Trait<'a, T> {} # struct Type<'b, U>(&'b U); # use std::fmt::{Debug, Display}; // impl mentions a 'static lifetime impl<'a, T: Debug, U: Display> Trait<'a, T> for Type<'static, U> {} ``` ``` # trait Trait<'a, T> {} # struct Type<'b, U>(&'b U); # use std::fmt::{Debug, Display}; // impl contains an outlives bound impl<'a, 'b, T: Debug, U: Display> Trait<'a, T> for Type<'b, U> where 'b: 'a {} ``` Impls that mention a generic parameter more than once are lifetime-depndent and produce false negatives, even if they don't expressly mention any lifetimes: ```rust # trait Trait<T> {} // impl mentions T more than once, creating an implied lifetime dependence impl<T> Trait<T> for T {} ``` The following impl is lifetime-**independent**, because even though it *mentions* lifetimes, implementation of the trait is not *conditional* over the lifetimes: ```rust # trait Trait<'a, T> {} # struct Type<'b, U>(&'b U); # use std::fmt::{Debug, Display}; impl<'a, 'b, T: Debug, U: Display> Trait<'a, T> for Type<'b, U> {} ``` Impls without generic parameters at all are also lifetime-independent, as long as they contain no `'static` lifetimes. ## Builtin impls Builtin impls (like `impl Debug for dyn Debug`, or automatic implementations of `Send` and `Sync`) have various obscure rules and often are not fully generic. To simplify reasoning about what is allowed and what not, all builtin impls are rejected and will neither directly nor indirectly contribute to a `Some` result. # Compile-time failures Determining whether `T` can be coerced to the dyn trait type `U` requires compiler trait resolution. In some cases, that resolution can exceed the recursion limit, and compilation will fail instead of this function returning `None`. The input type `T` must outlive the lifetime `'a` on the `dyn Trait + 'a`. This is basically the same rule that forbids `let x: &dyn Trait + 'static = &&some_local_variable;` So if you see borrow check errors around `try_as_dyn`, think about whether a normal unsizing coercion would be possible at all if you were using concrete types or had bounds on the input type. # Examples Using `try_as_dyn` to use bytewise comparison instead of PartialEq for certain types, similar to the standard library's optimization for slices: ```rust #![feature(try_as_dyn)] use core::any::try_as_dyn; /// Compares two objects for equality, fn eq<T: PartialEq + ?Sized>(x: &T, y: &T) -> bool { if try_as_dyn::<T, dyn BytewiseEq>(&x).is_some() { // T implements BytewiseEq, so we cast the slices to u8 and compare their bytes // instead of calling PartialEq on each individual element. unsafe { // SAFETY: x and y are valid for reads of size_of::<T>() bytes // BytewiseEq trait guarantees we can interperet these bytes as u8's // and compare them for equality let x = &*core::ptr::slice_from_raw_parts( (&raw const *x).cast::<u8>(), core::mem::size_of_val(x), ); let y = &*core::ptr::slice_from_raw_parts( (&raw const *y).cast::<u8>(), core::mem::size_of_val(y), ); x == y } } else { // T does not implement BytewiseEq, or try_as_dyn returned a false negative. // Fallback to PartialEq. // // BytewiseEq guarantees bytewise comparison and PartialEq will produce the same // results, so our code behaves correctly if try_as_dyn produces false negatives. x == y } } /// Marker trait for types that can be compared for equality /// using a bytewise comparison (i.e. memcmp). /// /// Implementations must ensure the type contains no uninitialized bytes, /// and that a bytewise comparison will produce the same result as PartialEq. unsafe trait BytewiseEq {} unsafe impl BytewiseEq for u8 {} unsafe impl BytewiseEq for u16 {} unsafe impl BytewiseEq for u32 {} // u16 implements BytewiseEq, so eq::<u16> will use bytewise comparison // (unless try_as_dyn returns a false negative) assert!(eq(&5u16, &5u16)); // f32 does not implement BytewiseEq, so eq::<f32> will use element-wise comparison assert!(eq(&5f32, &5f32)); ``` Using `try_as_dyn` for debugging: ```rust #![feature(try_as_dyn)] use core::any::{try_as_dyn, type_name}; use core::fmt::Debug; /// Prints a value of type T, attempting to use its Debug implementation with try_as_dyn. fn debug_println<T: ?Sized>(x: &T) { if let Some(debug) = try_as_dyn::<T, dyn Debug>(x) { println!("{:?}", debug); } else { // T does not implement Debug, or try_as_dyn returned a false negative. // Print the name of the type instead. // // We're not relying on this for correctness; it's just for debugging, // so we can tolerate false negatives. println!("<{}>", type_name::<T>()); } } /// This type does not implement Debug. struct NoDebug; // Prints "Hello, world!" unless try_as_dyn returns a false negative. debug_println(&"Hello, world!"); // Prints the name of the type, since it does not have a Debug implementation. debug_println(&NoDebug); // The current implementation of try_as_dyn gives a false positive in this case! debug_println(&"Hello, world!" as &dyn Debug); ``` </details>
rust-bors Bot
pushed a commit
that referenced
this pull request
Sep 17, 2026
…uwer Rollup of 24 pull requests Successful merges: - #161596 (coretests: Add more pattern tests.) - #162177 (Properly implement the gpu-kernel ABI for amdgpu) - #162411 (Make Receiver `#[rustc_dyn_incompatible_trait]`) - #162760 (yeet alias new_from_def_id) - #162796 (libtest: do not early exit from test runners) - #162844 (Add loan reachability traces to polonius MIR dumps) - #162876 (Move operations out of `rustc_middle::query::job`) - #160108 (Stabilize `windows_process_extensions_main_thread_handle`) - #160212 (traits: Fix rigid alias liveness matching) - #160544 (Stabilize `feature(trim_prefix_suffix)` (`{str, [T], Path}::trim_prefix` and `{str, [T]}::trim_suffix`)) - #161305 (Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha) - #161838 (tests: accept LLVM 24 optimization in this test) - #162312 (core: Rewrite docs for try_as_dyn) - #162785 (Avoid creating overlapping assignments in MatchBranchSimplification) - #162805 (Add `must_use` lint to `ExitCode`) - #162825 (core: Add examples for `debug_closure_helpers`) - #162841 (enable asm tests for xtensa targets) - #162842 (reintroduce check RibKind::ConstParamTy did in direct consts) - #162845 (mgca: fix issue with mismatched array valtree/valtree tys) - #162856 (Stabilize CommandExt::show_window) - #162865 (Complex conjugate, negation and default) - #162874 (Add support for `annotate_snippets::snippet::AnnotationKind::Visible`) - #162881 (Simplify the macro for forwarding Decoder methods ) - #162888 (Fix a typo on the Armv7-R platform docs page)
rust-bors Bot
pushed a commit
that referenced
this pull request
Sep 17, 2026
…uwer Rollup of 23 pull requests Successful merges: - #161596 (coretests: Add more pattern tests.) - #162411 (Make Receiver `#[rustc_dyn_incompatible_trait]`) - #162760 (yeet alias new_from_def_id) - #162796 (libtest: do not early exit from test runners) - #162844 (Add loan reachability traces to polonius MIR dumps) - #162876 (Move operations out of `rustc_middle::query::job`) - #160108 (Stabilize `windows_process_extensions_main_thread_handle`) - #160212 (traits: Fix rigid alias liveness matching) - #160544 (Stabilize `feature(trim_prefix_suffix)` (`{str, [T], Path}::trim_prefix` and `{str, [T]}::trim_suffix`)) - #161305 (Use the entire type of a dropped local to compute variance (edge direction) for Polonius alpha) - #161838 (tests: accept LLVM 24 optimization in this test) - #162312 (core: Rewrite docs for try_as_dyn) - #162785 (Avoid creating overlapping assignments in MatchBranchSimplification) - #162805 (Add `must_use` lint to `ExitCode`) - #162825 (core: Add examples for `debug_closure_helpers`) - #162841 (enable asm tests for xtensa targets) - #162842 (reintroduce check RibKind::ConstParamTy did in direct consts) - #162845 (mgca: fix issue with mismatched array valtree/valtree tys) - #162856 (Stabilize CommandExt::show_window) - #162865 (Complex conjugate, negation and default) - #162874 (Add support for `annotate_snippets::snippet::AnnotationKind::Visible`) - #162881 (Simplify the macro for forwarding Decoder methods ) - #162888 (Fix a typo on the Armv7-R platform docs page)
rust-bors Bot
pushed a commit
that referenced
this pull request
Sep 17, 2026
Rollup merge of #162312 - jnkel:try_as_dyn-docs, r=oli-obk core: Rewrite docs for try_as_dyn Update the documentation for `try_as_dyn` to resolve confusion some people had about the feature, provide better examples, and promise less. In particular, this PR updates the documentation to never promise success, allowing for spurious failures for even "simple" cases. This reflects the general consensus in https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/try_as_dyn.20potential.20problematic.20implications/near/621331808, since the precise rules of the implementation can be quite subtle and difficult to explain to users. Instead, document the function as being best-effort and able to produce false negatives, with non-exhaustive examples of situations where it produces false negatives in practice. We can always make the guarantees stricter in the future. Also replaces the "animal-cat-dog" style example with more realistic use cases of `try_as_dyn` for performance and debugging. cc @oli-obk <details><summary>Rendered</summary> Returns `Some(&U)` if `T` can be coerced to the dyn trait type `U`. Otherwise, it returns `None`. > **Warning** > > This function is implemented on a best-effort basis. It is not always possible to determine whether a generic type implements a trait; thus, this function may produce false negatives, returning `None` even when `T` implements the requested trait. > > `try_as_dyn` is guaranteed to return `None` if `T` does *not* implement the requested trait, but it is never guaranteed to return `Some`. It is intended to be used for performance optimizations and debugging, and `try_as_dyn` succeeding for a particular type should never be relied upon for correctness (i.e. callers must behave correctly even if `try_as_dyn` spuriously returns `None`). </div> # Examples of false negatives Some examples of situations where `try_as_dyn::<T, dyn Trait>` returns `None` in practice even when `T` implements `Trait`: * `T`'s impl for `Trait` is lifetime-dependent * `T`'s impl for `Trait` is a builtin impl (e.g. `dyn Debug` implements `Debug`) * `T`'s impl for `Trait` has a trait bound which requires transitively reasoning about lifetime-dependent or builtin impls This list is not exhaustive. There is some detailed documentation about these limitations at <https://doc.rust-lang.org/unstable-book/library-features/try-as-dyn.html> But the gist is summarized below: ## Lifetime-dependent impls `try_as_dyn` does not have access to lifetime information, thus it cannot differentiate between `'static` and other lifetimes and cannot reason about outlives bounds on impls. Thus it cannot reason about impls that have `'static` lifetimes or outlives bounds of any kind. /// The following impls are lifetime-dependent and produce false negatives when used with `try_as_dyn`: ```rust # trait Trait<'a, T> {} # struct Type<'b, U>(&'b U); # use std::fmt::{Debug, Display}; // impl mentions a 'static lifetime impl<'a, T: Debug, U: Display> Trait<'a, T> for Type<'static, U> {} ``` ``` # trait Trait<'a, T> {} # struct Type<'b, U>(&'b U); # use std::fmt::{Debug, Display}; // impl contains an outlives bound impl<'a, 'b, T: Debug, U: Display> Trait<'a, T> for Type<'b, U> where 'b: 'a {} ``` Impls that mention a generic parameter more than once are lifetime-depndent and produce false negatives, even if they don't expressly mention any lifetimes: ```rust # trait Trait<T> {} // impl mentions T more than once, creating an implied lifetime dependence impl<T> Trait<T> for T {} ``` The following impl is lifetime-**independent**, because even though it *mentions* lifetimes, implementation of the trait is not *conditional* over the lifetimes: ```rust # trait Trait<'a, T> {} # struct Type<'b, U>(&'b U); # use std::fmt::{Debug, Display}; impl<'a, 'b, T: Debug, U: Display> Trait<'a, T> for Type<'b, U> {} ``` Impls without generic parameters at all are also lifetime-independent, as long as they contain no `'static` lifetimes. ## Builtin impls Builtin impls (like `impl Debug for dyn Debug`, or automatic implementations of `Send` and `Sync`) have various obscure rules and often are not fully generic. To simplify reasoning about what is allowed and what not, all builtin impls are rejected and will neither directly nor indirectly contribute to a `Some` result. # Compile-time failures Determining whether `T` can be coerced to the dyn trait type `U` requires compiler trait resolution. In some cases, that resolution can exceed the recursion limit, and compilation will fail instead of this function returning `None`. The input type `T` must outlive the lifetime `'a` on the `dyn Trait + 'a`. This is basically the same rule that forbids `let x: &dyn Trait + 'static = &&some_local_variable;` So if you see borrow check errors around `try_as_dyn`, think about whether a normal unsizing coercion would be possible at all if you were using concrete types or had bounds on the input type. # Examples Using `try_as_dyn` to use bytewise comparison instead of PartialEq for certain types, similar to the standard library's optimization for slices: ```rust #![feature(try_as_dyn)] use core::any::try_as_dyn; /// Compares two objects for equality, fn eq<T: PartialEq + ?Sized>(x: &T, y: &T) -> bool { if try_as_dyn::<T, dyn BytewiseEq>(&x).is_some() { // T implements BytewiseEq, so we cast the slices to u8 and compare their bytes // instead of calling PartialEq on each individual element. unsafe { // SAFETY: x and y are valid for reads of size_of::<T>() bytes // BytewiseEq trait guarantees we can interperet these bytes as u8's // and compare them for equality let x = &*core::ptr::slice_from_raw_parts( (&raw const *x).cast::<u8>(), core::mem::size_of_val(x), ); let y = &*core::ptr::slice_from_raw_parts( (&raw const *y).cast::<u8>(), core::mem::size_of_val(y), ); x == y } } else { // T does not implement BytewiseEq, or try_as_dyn returned a false negative. // Fallback to PartialEq. // // BytewiseEq guarantees bytewise comparison and PartialEq will produce the same // results, so our code behaves correctly if try_as_dyn produces false negatives. x == y } } /// Marker trait for types that can be compared for equality /// using a bytewise comparison (i.e. memcmp). /// /// Implementations must ensure the type contains no uninitialized bytes, /// and that a bytewise comparison will produce the same result as PartialEq. unsafe trait BytewiseEq {} unsafe impl BytewiseEq for u8 {} unsafe impl BytewiseEq for u16 {} unsafe impl BytewiseEq for u32 {} // u16 implements BytewiseEq, so eq::<u16> will use bytewise comparison // (unless try_as_dyn returns a false negative) assert!(eq(&5u16, &5u16)); // f32 does not implement BytewiseEq, so eq::<f32> will use element-wise comparison assert!(eq(&5f32, &5f32)); ``` Using `try_as_dyn` for debugging: ```rust #![feature(try_as_dyn)] use core::any::{try_as_dyn, type_name}; use core::fmt::Debug; /// Prints a value of type T, attempting to use its Debug implementation with try_as_dyn. fn debug_println<T: ?Sized>(x: &T) { if let Some(debug) = try_as_dyn::<T, dyn Debug>(x) { println!("{:?}", debug); } else { // T does not implement Debug, or try_as_dyn returned a false negative. // Print the name of the type instead. // // We're not relying on this for correctness; it's just for debugging, // so we can tolerate false negatives. println!("<{}>", type_name::<T>()); } } /// This type does not implement Debug. struct NoDebug; // Prints "Hello, world!" unless try_as_dyn returns a false negative. debug_println(&"Hello, world!"); // Prints the name of the type, since it does not have a Debug implementation. debug_println(&NoDebug); // The current implementation of try_as_dyn gives a false positive in this case! debug_println(&"Hello, world!" as &dyn Debug); ``` </details>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Update the documentation for
try_as_dynto resolve confusion some people had about the feature, provide better examples, and promise less.In particular, this PR updates the documentation to never promise success, allowing for spurious failures for even "simple" cases. This reflects the general consensus in https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/try_as_dyn.20potential.20problematic.20implications/near/621331808, since the precise rules of the implementation can be quite subtle and difficult to explain to users. Instead, document the function as being best-effort and able to produce false negatives, with non-exhaustive examples of situations where it produces false negatives in practice. We can always make the guarantees stricter in the future.
Also replaces the "animal-cat-dog" style example with more realistic use cases of
try_as_dynfor performance and debugging.cc @oli-obk
Rendered
Returns
Some(&U)ifTcan be coerced to the dyn trait typeU. Otherwise, it returnsNone.Examples of false negatives
Some examples of situations where
try_as_dyn::<T, dyn Trait>returnsNonein practice evenwhen
TimplementsTrait:T's impl forTraitis lifetime-dependentT's impl forTraitis a builtin impl (e.g.dyn DebugimplementsDebug)T's impl forTraithas a trait bound which requires transitively reasoning aboutlifetime-dependent or builtin impls
This list is not exhaustive. There is some detailed documentation about these limitations at
https://doc.rust-lang.org/unstable-book/library-features/try-as-dyn.html But the gist is
summarized below:
Lifetime-dependent impls
try_as_dyndoes not have access to lifetime information, thus it cannot differentiate between'staticand other lifetimes and cannot reason about outlives bounds on impls. Thus it cannotreason about impls that have
'staticlifetimes or outlives bounds of any kind. ///The following impls are lifetime-dependent and produce false negatives when used with
try_as_dyn:Impls that mention a generic parameter more than once are lifetime-depndent and produce false
negatives, even if they don't expressly mention any lifetimes:
The following impl is lifetime-independent, because even though it mentions lifetimes,
implementation of the trait is not conditional over the lifetimes:
Impls without generic parameters at all are also lifetime-independent, as long as they contain
no
'staticlifetimes.Builtin impls
Builtin impls (like
impl Debug for dyn Debug, or automatic implementations ofSendandSync) have various obscure rules and often are not fully generic. To simplify reasoning aboutwhat is allowed and what not, all builtin impls are rejected and will neither directly nor
indirectly contribute to a
Someresult.Compile-time failures
Determining whether
Tcan be coerced to the dyn trait typeUrequires compiler trait resolution.In some cases, that resolution can exceed the recursion limit,
and compilation will fail instead of this function returning
None.The input type
Tmust outlive the lifetime'aon thedyn Trait + 'a.This is basically the same rule that forbids
let x: &dyn Trait + 'static = &&some_local_variable;So if you see borrow check errors around
try_as_dyn, think about whether a normal unsizingcoercion would be possible at all if you were using concrete types or had bounds on the input type.
Examples
Using
try_as_dynto use bytewise comparison instead of PartialEq for certain types, similar tothe standard library's optimization for slices:
Using
try_as_dynfor debugging: