Skip to content

Conversation

@jackh726
Copy link
Member

It was little tricky when trying to describe diverging blocks. The compiler's implementation maintains sort of a "global state" when checking an expression and sub-expressions, which it resets on conditional things. Semantically, I think the way I worded it is much clearer than trying to match the implementation.

Happy to hear any specific feedback, Lcnr made did an initial review pass in rust-lang/project-goal-reference-expansion@4079171, so the second commit tries to address that.

@rustbot rustbot added the S-waiting-on-review Status: The marked PR is awaiting review from a maintainer label Oct 28, 2025
@rust-cloud-vms rust-cloud-vms bot force-pushed the reference-type-inference-divergence branch from 3b0ec86 to 920cec4 Compare October 29, 2025 14:14
@rust-cloud-vms rust-cloud-vms bot force-pushed the reference-type-inference-divergence branch from 920cec4 to d020656 Compare October 29, 2025 14:24
@traviscross
Copy link
Contributor

Thanks for the PR @jackh726; I can tell this was written carefully. It will be good to get more of this documented. In particular, it'll be good to have the fallback behavior documented.

I'll leave some notes inline. Probably we'll want to move some things around.

Adding more examples -- even beyond what I'll note specifically inline -- would be particularly good for this material. It's helpful when each rule has one or more concise and testable examples demonstrating exactly what the rule means to express.

- [Subtyping and variance](subtyping.md)
- [Trait and lifetime bounds](trait-bounds.md)
- [Type coercions](type-coercions.md)
- [Divergence](divergence.md)
Copy link
Contributor

@traviscross traviscross Oct 29, 2025

Choose a reason for hiding this comment

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

We try to keep the number of top-level chapters contained. Looking at it, perhaps most of what's here that can't be inlined on the pages for each expression would make sense appearing in the Expressions chapter (e.g., we talk about place and value expressions there -- the verbiage about when a place expression is diverging might make sense near that) and in the Never type chapter.

Comment on lines +99 to +100
r[expr.match.type]
The type of the overall `match` expression is the [least upper bound](../type-coercions.md#r-coerce.least-upper-bound) of the individual match arms.
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're inlining the rule about determining the type based on the LUB for match, from https://doc.rust-lang.org/1.90.0/reference/type-coercions.html#r-coerce.least-upper-bound.intro, probably we'd need to do that for the other rules there also (and then either remove the list from there or convert it to an admonition or index).

Copy link
Contributor

Choose a reason for hiding this comment

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

As an aside, looking into this rule is what prompted me to file:

@jackh726
Copy link
Member Author

Thanks for the review @traviscross. Good points here, I'll work on sorting through them today/tomorrow.

Happy to jump on a call at some point too, if you think any of these could use further discussion.

@rust-cloud-vms rust-cloud-vms bot force-pushed the reference-type-inference-divergence branch from c99414f to a11338f Compare November 1, 2025 21:57
@rust-cloud-vms rust-cloud-vms bot force-pushed the reference-type-inference-divergence branch from a11338f to a9d8264 Compare November 1, 2025 21:59
Comment on lines 95 to 105
struct Foo {
x: !,
}

let foo = Foo { x: make() };
let diverging_place_not_read: () = {
let _: () = {
// Asssignment to `_` means the place is not read
let _ = foo.x;
};
};
Copy link
Contributor

@traviscross traviscross Nov 2, 2025

Choose a reason for hiding this comment

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

This compiles and produces an infinite loop.

Playground link

But then, so does:

    let foo = Foo { x: make() };
    let diverging_place_not_read: () = {
        let _: () = {
            // Asssignment to something other than `_` means?
            let _x = foo.x;
        };
    };

Playground link

Is there a way to demonstrate this such that let _ = .. produces distinct compile-time or runtime behavior from let _x = ..?

Copy link
Contributor

@traviscross traviscross Nov 2, 2025

Choose a reason for hiding this comment

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

To answer the question posed, this compiles:

trait RetId { type Ty; }
impl<T> RetId for fn() -> T { type Ty = T; }

struct S<T> {
    x: T,
}

fn f(x: S<<fn() -> ! as RetId>::Ty>) -> ! {
    let _x = x.x; // OK.
}

But this does not:

fn f(x: S<<fn() -> ! as RetId>::Ty>) -> ! {
    let _ = x.x; // ERROR: Mismatched types.
}

This is one, though, where it's not immediately coming to mind how to express this without either the never type or the never type hack.

Copy link
Contributor

@traviscross traviscross Nov 2, 2025

Choose a reason for hiding this comment

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

Here's one way. (Of course, this is really just another instantiation of the never type hack.)

fn phantom_call<T>(_: impl FnOnce(T) -> T) {}

let _ = phantom_call(|x| -> ! {
    let _x = x; // OK.
});
let _ = phantom_call(|x| -> ! {
    let _ = x; // ERROR: Mismatched types.
});

Copy link
Contributor

@traviscross traviscross Nov 2, 2025

Choose a reason for hiding this comment

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

It's interesting how it really does need the ! type to be ascribed for this to work. I.e., it doesn't work with:

struct W<T>(T);

let x = W(loop {});
let _ = || -> ! {
    let _x = x.0; // ERROR.
};

Any thoughts about the reason for that?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's interesting how it really does need the ! type to be ascribed for this to work. I.e., it doesn't work with:

struct W<T>(T);

let x = W(loop {});
let _ = || -> ! {
    let _x = x.0; // ERROR.
};

Any thoughts about the reason for that?

This one nearly stumped me, because I had my mind set that this was a closure thing. But then I realized, this also fails:

let _: ! = {
    let _x = x.0;
};

The important bit to remember here is that x.0 is an inference var which does not produce divergence (only place expressions that produce types of ! do). The "expectation" value of the block (either as the block's type, or the closure's return value) does not constrain that inference variable.

Copy link
Contributor

@traviscross traviscross Nov 10, 2025

Choose a reason for hiding this comment

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

Makes sense, thanks. (And yes, the only purpose of the closure in the examples is to be able to ascribe the type without nightly features.)

Coincidentally, prompted by lcnr apropos of lcnr's work, I was just looking at something else today with a similar flavor:

trait Tr: Sized {
    fn abs(self) -> i32 { 0 }
}
impl Tr for i32 {}

fn main() {
    let x = -42;
    let y: i32 = x.abs();
    assert!(y != x.abs()); // Surprising, but true.
}

@traviscross
Copy link
Contributor

@rustbot author

@rustbot
Copy link
Collaborator

rustbot commented Nov 2, 2025

Error: shortcut handler unexpectedly failed in this comment: failed to remove Label { name: "S-waiting-on-review" }

Please file an issue on GitHub at triagebot if there's a problem with this bot, or reach out on #triagebot on Zulip.

@traviscross traviscross added S-waiting-on-author Status: The marked PR is awaiting some action (such as code changes) from the PR author. and removed S-waiting-on-review Status: The marked PR is awaiting review from a maintainer labels Nov 2, 2025
The type of the overall `match` expression is the [least upper bound](../type-coercions.md#r-coerce.least-upper-bound) of the individual match arms.

r[expr.match.empty]
If there are no match arms, then the `match` expression is diverging and the type is [`!`](../types/never.md).
Copy link
Member

Choose a reason for hiding this comment

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

does this require a diverging scrutinee expression to compile?

probably worth adding an example either way

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a good question! The answer is (subtly) no. Take the following:

fn make<T>() -> T { loop {} }
enum Empty {}

fn diverging_match_no_arms() -> ! {
    let e: Empty = make();
    match e {}
}

e is uninhabited, but is not considered diverging from a type-checking standpoint. To make it more confusing, this also fires the unreachable_code lint, like diverging expressions, but not in the same code path

Adding this as an example.

Copy link
Member

Choose a reason for hiding this comment

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

lgtm, I was wondering if there would be a good place to link to the glossary item on uninhabited types as related but distinct but I couldn't find a good place. Wanted to mention the idea in case you can think of a way to fit it in and you think its valuable to do so.

@rust-cloud-vms rust-cloud-vms bot force-pushed the reference-type-inference-divergence branch 2 times, most recently from e33441f to 63775d7 Compare November 12, 2025 18:23
@rust-cloud-vms rust-cloud-vms bot force-pushed the reference-type-inference-divergence branch from 63775d7 to f9cf9f5 Compare November 12, 2025 18:25
Copy link
Contributor

@nikomatsakis nikomatsakis 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.

# #![ feature(never_type) ]
# fn make<T>() -> T { loop {} }
fn no_control_flow() -> ! {
// There are no conditional statements, so this entire block is diverging.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does "this entire block" refer to the block of the fn body? I found it a touch ambiguous.

# Divergence

r[divergence.intro]
Divergence is the state where a particular section of code could never be encountered at runtime. Importantly, while there are certain language constructs that immediately produce a _diverging expression_ of the type [`!`](./types/never.md), divergence can also propogate to the surrounding block.
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather say:

"Divergence is the state where a particular section of code will never terminate" -- although it too is approximate.

"If an expression diverges, then nothing after that expression will execute"

something like that I guess.

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

Labels

S-waiting-on-author Status: The marked PR is awaiting some action (such as code changes) from the PR author.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants