Skip to content

Remove 'static requirement on try_as_dyn#150161

Open
oli-obk wants to merge 3 commits intorust-lang:mainfrom
oli-obk:try_as_dyn_non_static
Open

Remove 'static requirement on try_as_dyn#150161
oli-obk wants to merge 3 commits intorust-lang:mainfrom
oli-obk:try_as_dyn_non_static

Conversation

@oli-obk
Copy link
Contributor

@oli-obk oli-obk commented Dec 19, 2025

@rustbot
Copy link
Collaborator

rustbot commented Dec 19, 2025

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

Some changes occurred to the core trait solver

cc @rust-lang/initiative-trait-system-refactor

Some changes occurred to the CTFE / Miri interpreter

cc @rust-lang/miri

@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 Dec 19, 2025
@rustbot rustbot added the WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) label Dec 19, 2025
@rustbot
Copy link
Collaborator

rustbot commented Dec 19, 2025

r? @SparrowLii

rustbot has assigned @SparrowLii.
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

@rust-log-analyzer

This comment has been minimized.

@oli-obk oli-obk force-pushed the try_as_dyn_non_static branch 2 times, most recently from e7ef1ee to 29f1dba Compare December 19, 2025 16:44
Copy link
Contributor

@danielhenrymantilla danielhenrymantilla left a comment

Choose a reason for hiding this comment

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

Some drive-by comments; but I'm not rustc/HIR-savy, so take these with a grain of salt 🙇

View changes since this review

@BoxyUwU
Copy link
Member

BoxyUwU commented Dec 19, 2025

r? BoxyUwU

@rustbot rustbot assigned BoxyUwU and unassigned SparrowLii Dec 19, 2025
@theemathas

This comment has been minimized.

@theemathas

This comment has been minimized.

@theemathas

This comment has been minimized.

@BoxyUwU BoxyUwU 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 Dec 30, 2025
@oli-obk oli-obk force-pushed the try_as_dyn_non_static branch from 29f1dba to fe33b0c Compare January 7, 2026 12:41
@rustbot

This comment has been minimized.

@oli-obk oli-obk force-pushed the try_as_dyn_non_static branch 3 times, most recently from dfa5c33 to 30f5641 Compare January 7, 2026 12:44
@rust-log-analyzer

This comment has been minimized.

@oli-obk
Copy link
Contributor Author

oli-obk commented Jan 13, 2026

The hacky solution is obviously not a general fix. But I think it's progress. As a next step I will add the input type as a generic parameter on TryAsDynCompat, at which point we should be able to enforce (in borrowck) that the input type outlives any lifetimes on the dyn Trait or its generic parameters. So if a generic parameter T has a 'static bound, it could be used as an input type for a try_as_dyn irrespective of the bounds on the dyn Trait. In the other direction, we will likely end up rejecting many traits that have generic parameters as the bounds are not something that can be written in Rust.

@theemathas

This comment was marked as resolved.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@theemathas
Copy link
Contributor

trait_info_of_trait_type_id can be given a TypeId for a trait object type that doesn't implement TryAsDynCompatible. It should probably be at least noted that there could be false positives or something.

@rust-bors

This comment has been minimized.

@theemathas
Copy link
Contributor

theemathas commented Mar 1, 2026

I haven't given this enough thought to be sure, but I suspect that we can allow all where Type: Trait bounds in fully-generic impls, as long as each individual bound uses each generic type parameter and each lifetime parameter only once.

Edit: I've thought about it more. I think that this works, except that we still need to keep prohibiting generics inside ClauseKind::{RegionOutlives, TypeOutlives, Projection} bounds.

@theemathas
Copy link
Contributor

theemathas commented Mar 1, 2026

The invariant is that each Type: Trait obligation we generate does not mention 'static, and does not use the same generic type parameter or lifetime parameter twice.

Edit: A Type: Trait bound, however, may mention the same bound (HRTB) lifetime any number of times.

@theemathas
Copy link
Contributor

theemathas commented Mar 2, 2026

This code feels really close to an unsoundness. It panics at run time at the unwrap, but I don't understand why. Could you explain how the trait solver manages to correctly figure out that the type doesn't implement the trait?

#![feature(try_as_dyn)]

use core::any::try_as_dyn;

trait HasAssoc<'a> {
    type Assoc;
}
struct Dummy;
impl<'a> HasAssoc<'a> for Dummy {
    // Changing this to &'a i64 makes the code not panic
    type Assoc = &'static i64;
}

trait Trait {}
impl Trait for i32 where for<'a> Dummy: HasAssoc<'a, Assoc = &'a i64> {}

fn main() {
    let x = 1i32;
    let _: &dyn Trait = try_as_dyn(&x).unwrap();
}

Edit: I think it's because the trait solver probably can distinguish bound lifetimes from each other and from 'static

@theemathas theemathas 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 2, 2026
@oli-obk oli-obk force-pushed the try_as_dyn_non_static branch from d08dd2f to c6f08ed Compare March 2, 2026 14:51
@rustbot

This comment has been minimized.

@oli-obk oli-obk force-pushed the try_as_dyn_non_static branch from c6f08ed to 1bb0baa Compare March 2, 2026 14:51
@rust-log-analyzer

This comment has been minimized.

@oli-obk oli-obk force-pushed the try_as_dyn_non_static branch from 1bb0baa to d2cc847 Compare March 2, 2026 15:41
@rust-log-analyzer

This comment has been minimized.

@theemathas
Copy link
Contributor

theemathas commented Mar 3, 2026

Here are the current rules for what counts as a "fully generic impl". I think they should be documented in the public API doc comments.


For the purposes of defining fully generic impls, we'll define the following:

  • A "lifetime-infected parameter" is a generic type parameter T or lifetime parameter 'a declared in the impl<....> angle brackets.
    • This does not include const generics, and does not include for<'a> lifetimes.
  • A "where bound" is an explicitly written trait or lifetime bound (Type: Trait, Type: 'a, or 'a: 'b) in the trait header, including ones written in the impl<....> angle brackets without the where keyword.
    • This does not include any additional implicit bounds that are implied by Type: Trait bounds, such as via supertraits, associated type bounds in the trait definition, or well-formedness of types.
    • A bound such as Type: Trait1 + Trait2 is considered to be two separate where bounds: Type: Trait1 and Type: Trait2.
    • A bound such as Type: Trait<Assoc: Trait2> is desugared into two separate where bounds: Type: Trait and <Type as Trait>::Assoc: Trait2. (Note that the latter where bound is prohibited in fully generic impls, due to having an associated type projection.)

An impl block is a fully generic impl iff all the following conditions apply:

  • The impl block is a handwritten impl, as opposed to a type implementing a trait automatically by the compiler.
    • Examples of types automatically implementing traits include: types automatically implementing auto traits, dyn Trait implementing Trait, and closures implementing Fn traits.
    • derives are considered handwritten impls.
  • 'static is not mentioned anywhere in the impl block header.
  • Associated type projections (<Type as Trait>::Assoc) are not mentioned anywhere in the impl block header.
  • Each outlives where bound (Type: 'a and 'a: 'b) does not mention lifetime-infected parameters.
  • Each trait where bound with an associated type equality (Type: Trait<Assoc = Type2>) does not mention lifetime-infected parameters.
  • Each lifetime-infected parameter is mentioned at most once in the Self type and the implemented trait's generic parameters, combined.
  • Each trait where bound (Type: Trait) does not mention lifetime-infected parameters on the right hand side.
  • Each trait where bound (Type: Trait) either has a generic type parameter on its own on the left hand side (T: Trait), or does not mention lifetime-infected parameters on the left hand side.

My proposed rule change replaces the last two bullet points with the following, which I think makes the rules easier to understand:

  • Each individual trait where bound (Type: Trait) mentions each lifetime-infected parameter at most once. (Mentioning a lifetime-infected parameter in multiple where bounds is allowed.)

Edit: As per @danielhenrymantilla's suggestion below, I think that restrictions involving lifetime-infected parameters in where Type: Trait bounds can be removed completely.

@theemathas
Copy link
Contributor

I would like to rename "fully generic impl" to "lifetime-independent impl".

@danielhenrymantilla
Copy link
Contributor

💯 I was gonna suggest just that, @theemathas: a lifetime-independent, or rather, lifetime-agnostic, impl. If using "fully", I think the term used historically is that of it being a blanket impl, so "fully blanket over lifetimes".

@danielhenrymantilla
Copy link
Contributor

danielhenrymantilla commented Mar 3, 2026

We could also try and expand on "exploit" scenarios for each of these rules, should it be missing, that is, some form of rationale as to why each of these rules is (plausibly) necessary (the whole other question being whether these rules are sufficient for soundness, which currently stems from "we haven't been able to fathom other ways to amount to lifetime discrimination").

Plausible reasons for the necessity of these rules

Important

The main problem of try_as_dyn and specialization is the compiler's inability, while trait-checking, to distinguish/discriminate between any two given lifetimes1.

  • 'static is not mentioned anywhere in the impl block header.

    The most obvious one: if you have impl IsStatic for &'static str, then determining whether &'? str : IsStatic does hold amounts to discriminating '? : 'static.

  • Each outlives where bound (Type: 'a and 'a: 'b) does not mention lifetime-infected parameters.

    We can create lifetime discrimination this way. For instance, given impl<'a, 'b> Outlives<'a> for &'b str where 'b : 'a {}, Outlives<'static> amounts to IsStatic from previous bullet.

  • Each lifetime-infected parameter is mentioned at most once in the Self type and the implemented trait's generic parameters, combined.

    Repetition of a parameter entails equality of those two use-sites; in lifetime-terms, this would be a double 'a : 'b / 'b : 'a clause, for instance.
    Follow-up from the previous example: impl<'a> Uses<'a> for &'a str {}, and check whether &'? str : Uses<'static>.

  • Each individual trait where bound (Type: Trait) mentions each lifetime-infected parameter at most once.
    (Mentioning a lifetime-infected parameter in multiple where bounds is allowed.)

    Click to see

    Looking at the previous rules, which focuses on Self : …, this is just observing that shifting the requirements to other parameters within where clauses [ought to] boil down to the same set of issues.

    TODO: add a proper counter-example (cc @theemathas, do you have an easy one in mind?)

    EDIT: I am starting to think this is unnecessarily restrictive: we should be able to loosen it up somehow. Repetition only in where clauses seems fine.

    EDIT 2: 👇

    This has since been confirmed to be an unnecessary rule.

  • The impl block is a handwritten impl, as opposed to a type implementing a trait automatically by the compiler (such as auto-traits, dyn Bounds… : Bounds…, and closures)
    The reason for this is that some such auto-generated impls come with hidden bounds or whatnot, which run afoul of the previous rules, whilst also being extremely challenging for the current compiler logic to know of such bounds.
    IIUC, this restriction could be lifted in the future should the compiler logic be better at spotting these hidden bounds, when present.

    • One contrived such example being the case of dyn 'u + for<'a> Outlives<'a>, where the compiler-generated impl for it of Outlives is: impl<'b, 'u> Outlives<'b> for dyn 'u + for<'a> Outlives<'a> where 'b : 'u {} which violates the "'a: 'b not to mention lt-infected params" rule, whilst also being hard to detect in current compiler logic.
  • Associated type projections (<Type as Trait>::Assoc) are not mentioned anywhere in the impl block header.

    Associated type projections can "inject" lifetimes such as 'static. For instance, checking whether &'? str: Trait discriminates '? against 'static in the following scenario:

    trait Assoc { type StaticStr; }
    impl Assoc for () { type StaticStr = &'static str; }
    impl Trait for <() as Assoc>::StaticStr {}
  • Each trait where bound with an associated type equality (Type: Trait<Assoc = Type2>) does not mention lifetime-infected parameters.

    Checking whether Option<&'? str>: IntoIterator<Item = &'static str> holds discriminates '? against 'static.

Footnotes

  1. the compiler cannot branch on whether "'a : 'b holds": for soundness, it can either choose not to know the answer, or assume that it holds and produce an obligation for the borrow-checker which shall "assert this" (making compilation fail in a fatal manner if not). Most usages of Rust lie in the latter category (typical where clauses anywhere), whilst specialization/try_as_dyn() wants to support fallibility of the operation (i.e., being queried on a type not fulfilling the predicate without causing a compilation error). This rules out the latter, resulting in the need for the former, i.e., for the try_as_dyn() attempt to unconditionally "fail" with None.

@theemathas
Copy link
Contributor

theemathas commented Mar 3, 2026

I agree with @danielhenrymantilla. The repetition restriction on where Type: Trait bounds can be removed.

The problem with repeating lifetime-infected parameters is that, when figuring out the parameter substitutions for any given impl, we need to unify the "user-specified types" with the Self type and the trait's generic parameters. If there are repetitions (within the Self type combined with the trait's generics), then this process can cause two different lifetimes to be unified into the same lifetime-infected parameter, which is a problem.

In contrast, repeating lifetime-infected parameters in a where Type: Trait bound can only cause a single lifetime to be unified into two different lifetime-infected parameters, which doesn't cause any issues.

@oli-obk oli-obk force-pushed the try_as_dyn_non_static branch from d2cc847 to c6abcad Compare March 10, 2026 11:43
@rustbot
Copy link
Collaborator

rustbot commented Mar 10, 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.

@oli-obk
Copy link
Contributor Author

oli-obk commented Mar 10, 2026

Thanks @danielhenrymantilla for writing down this summary. I haven't addressed the concern about the restriction yet, but I included your summary mostly verbatim in the unstable book

@rust-log-analyzer
Copy link
Collaborator

The job aarch64-gnu-llvm-20-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
/dev/sda15       98M  6.4M   92M   7% /boot/efi
tmpfs           1.6G   16K  1.6G   1% /run/user/1001
================================================================================

Sufficient disk space available (120767720KB >= 52428800KB). Skipping cleanup.
##[group]Run echo "[CI_PR_NUMBER=$num]"
echo "[CI_PR_NUMBER=$num]"
shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
---
set -ex

# Run a subset of tests. Used to run tests in parallel in multiple jobs.

# When this job partition is run as part of PR CI, skip tidy to allow revealing more failures. The
# dedicated `tidy` job failing won't block other PR CI jobs from completing, and so tidy failures
# shouldn't inhibit revealing other failures in PR CI jobs.
if [ "$PR_CI_JOB" == "1" ]; then
  echo "PR_CI_JOB set; skipping tidy"
  SKIP_TIDY="--skip tidy"
fi

../x.py --stage 2 test \
  ${SKIP_TIDY:+$SKIP_TIDY} \
  --skip compiler \
  --skip src
#!/bin/bash

set -ex

# Run a subset of tests. Used to run tests in parallel in multiple jobs.

# When this job partition is run as part of PR CI, skip tidy to allow revealing more failures. The
# dedicated `tidy` job failing won't block other PR CI jobs from completing, and so tidy failures
# shouldn't inhibit revealing other failures in PR CI jobs.
if [ "$PR_CI_JOB" == "1" ]; then
  echo "PR_CI_JOB set; skipping tidy"
  SKIP_TIDY="--skip tidy"
fi

../x.py --stage 2 test \
  ${SKIP_TIDY:+$SKIP_TIDY} \
  --skip tests \
  --skip coverage-map \
  --skip coverage-run \
  --skip library \
  --skip tidyselftest
---
##[endgroup]
Executing "/scripts/stage_2_test_set2.sh"
+ /scripts/stage_2_test_set2.sh
+ '[' 1 == 1 ']'
+ echo 'PR_CI_JOB set; skipping tidy'
+ SKIP_TIDY='--skip tidy'
+ ../x.py --stage 2 test --skip tidy --skip tests --skip coverage-map --skip coverage-run --skip library --skip tidyselftest
PR_CI_JOB set; skipping tidy
##[group]Building bootstrap
    Finished `dev` profile [unoptimized] target(s) in 0.04s
##[endgroup]
---
[RUSTC-TIMING] corebenches test:true 9.302
[RUSTC-TIMING] allocbenches test:true 7.833
[RUSTC-TIMING] alloctests test:true 18.314
[RUSTC-TIMING] alloctests test:true 18.633
error[E0080]: evaluation panicked: assertion failed: TypeId::of::<Garlic>().trait_info_of::<dyn Blah + Send>().is_some()
##[error]  --> coretests/tests/mem/trait_info_of.rs:18:9
   |
18 |         assert!(TypeId::of::<Garlic>().trait_info_of::<dyn Blah + Send>().is_some());
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         |
   |         evaluation of `mem::trait_info_of::test_implements_trait::{constant#0}` failed here
   |         in this macro invocation
   |
  --> library/core/src/macros/mod.rs:1687:4
   |
   = note: in this expansion of `assert!`

note: erroneous constant encountered
  --> coretests/tests/mem/trait_info_of.rs:16:5
   |
16 | /     const {
17 | |         assert!(TypeId::of::<Garlic>().trait_info_of::<dyn Blah>().is_some());
18 | |         assert!(TypeId::of::<Garlic>().trait_info_of::<dyn Blah + Send>().is_some());
19 | |         assert!(TypeId::of::<*const Box<Garlic>>().trait_info_of::<dyn Sync>().is_none());
20 | |         assert!(TypeId::of::<u8>().trait_info_of_trait_type_id(TypeId::of::<dyn Blah>()).is_none());
21 | |     }
   | |_____^

For more information about this error, try `rustc --explain E0080`.
[RUSTC-TIMING] coretests test:true 64.385
error: could not compile `coretests` (test "coretests") due to 1 previous error
env -u RUSTC_WRAPPER CARGO_ENCODED_RUSTDOCFLAGS="-Csymbol-mangling-version=v0\u{1f}-Zannotate-moves\u{1f}-Zrandomize-layout\u{1f}-Zunstable-options\u{1f}--check-cfg=cfg(bootstrap)\u{1f}-Wrustdoc::invalid_codeblock_attributes\u{1f}--crate-version\u{1f}1.96.0-nightly\t(7ac6c8d87\t2026-03-10)" CARGO_ENCODED_RUSTFLAGS="-Csymbol-mangling-version=v0\u{1f}-Zannotate-moves\u{1f}-Zrandomize-layout\u{1f}-Zunstable-options\u{1f}--check-cfg=cfg(bootstrap)\u{1f}-Zmacro-backtrace\u{1f}-Csplit-debuginfo=off\u{1f}-Clink-arg=-L/usr/lib/llvm-20/lib\u{1f}-Cllvm-args=-import-instr-limit=10\u{1f}-Clink-args=-Wl,-z,origin\u{1f}-Clink-args=-Wl,-rpath,$ORIGIN/../lib\u{1f}--cap-lints=allow\u{1f}--cfg\u{1f}randomized_layouts" RUSTC="/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/dist/rustc-clif" RUSTDOC="/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/dist/rustdoc-clif" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage0/bin/cargo" "test" "--manifest-path" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/build/sysroot_tests/Cargo.toml" "--target-dir" "/checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif/build/sysroot_tests_target" "--locked" "--target" "aarch64-unknown-linux-gnu" "-p" "coretests" "-p" "alloctests" "--tests" "--" "-q" exited with status ExitStatus(unix_wait_status(25856))
Command `/checkout/obj/build/aarch64-unknown-linux-gnu/stage0/bin/cargo run -Zwarnings --target aarch64-unknown-linux-gnu -Zbinary-dep-depinfo -j 4 -Zroot-dir=/checkout --locked --color=always --profile=release --manifest-path /checkout/compiler/rustc_codegen_cranelift/build_system/Cargo.toml -- test --download-dir /checkout/obj/build/cg_clif_download --out-dir /checkout/obj/build/aarch64-unknown-linux-gnu/stage2-codegen/cg_clif --no-unstable-features --use-backend cranelift --sysroot llvm --skip-test testsuite.extended_sysroot [workdir=/checkout/compiler/rustc_codegen_cranelift]` failed with exit code 1
Created at: src/bootstrap/src/core/build_steps/test.rs:3913:25
Executed at: src/bootstrap/src/core/build_steps/test.rs:3958:26

Command has failed. Rerun with -v to see more details.
Bootstrap failed while executing `--stage 2 test --skip tidy --skip tests --skip coverage-map --skip coverage-run --skip library --skip tidyselftest`
Build completed unsuccessfully in 0:26:06
  local time: Tue Mar 10 12:19:05 UTC 2026
  network time: Tue, 10 Mar 2026 12:19:05 GMT
##[error]Process completed with exit code 1.


### Each trait `where` bound with an associated type equality (`Type: Trait<Assoc = Type2>`) does not mention lifetime-infected parameters.

Checking whether `Option<&'? str>: IntoIterator<Item = &'static str>` holds discriminates `'?` against `'static`.
Copy link
Contributor

Choose a reason for hiding this comment

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

This would already run into the "no 'static rule". I'm unsure if this rule is needed or not.

Copy link
Contributor

@danielhenrymantilla danielhenrymantilla Mar 11, 2026

Choose a reason for hiding this comment

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

Very true, let's go with the following example, then:

impl<'a, 'b> Trait<'b> for &'a str
where
    Option<&'a str>: IntoIterator<Item = &'b str>,
{}

which amounts to having done:

impl<'a> Trait<'a> for &'a str {}

which brings us back to "no repetitions in header" (&'? str: Trait<'static> discriminates '? against 'static).


Suggested change
Checking whether `Option<&'? str>: IntoIterator<Item = &'static str>` holds discriminates `'?` against `'static`.
Associated-type equality bounds can very much amount to lifetime-infected parameter equality constraints, which are problematic as per the "at most one mention of each lifetime-infected parameter in header" rule.
To illustrate, with the following definitions, `&'? str: Trait<'static>` discriminates `'?` against `'static`:
```rust
impl<'a, 'b> Trait<'b> for &'a str
where
// &'a str = &'b str,
Option<&'a str>: IntoIterator<Item = &'b str>,
{}
impl<'a> Trait<'a> for &'a str {}
```


The most obvious one: if you have `impl IsStatic for &'static str`, then determining whether `&'? str : IsStatic` does hold amounts to discriminating `'? : 'static`.

### Each outlives `where` bound (`Type: 'a` and `'a: 'b`) does not mention lifetime-infected parameters.
Copy link
Contributor

Choose a reason for hiding this comment

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

This page doesn't define "lifetime-infected parameter" anywhere.

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. T-libs Relevant to the library team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants