Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make typeck aware of uninhabited types #108993

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

cjgillot
Copy link
Contributor

Rebase of #100288, thanks @camsteffen for doing the hard work!

This PR modifies the unreachable code lint to operate from within typeck, based on inhabitedness of types discovered as the analysis goes. As a consequence, the lint fires in many extra places. Constructors are carved out on purpose, to avoid an "unreachable call" diagnostic on them, which is not very useful as they are inert.

In addition, inhabitedness information is used from within typeck to guide inference. This allows the following snippet to compile, as the compiler is not able to prove that loop_break_void diverges on all paths.

enum Void {}

fn get_void() -> Void {
    panic!()
}

fn loop_break_void() -> i32 {
    let loop_value = loop { break get_void() };
}

Inhabitedness information is privacy aware, so this is only possible if Void is visibly uninhabited at the use site. See tests/ui/break-diverging-value.rs test for a case where it is not.

@cjgillot cjgillot added the I-lang-nominated Nominated for discussion during a lang team meeting. label Mar 10, 2023
@rustbot
Copy link
Collaborator

rustbot commented Mar 10, 2023

r? @michaelwoerister

(rustbot has picked a reviewer for you, use r? to override)

@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. labels Mar 10, 2023
@rustbot
Copy link
Collaborator

rustbot commented Mar 10, 2023

Some changes occurred in src/tools/rust-analyzer

cc @rust-lang/wg-rls-2

@bors
Copy link
Contributor

bors commented Mar 11, 2023

☔ The latest upstream changes (presumably #109015) made this pull request unmergeable. Please resolve the merge conflicts.

@rustbot
Copy link
Collaborator

rustbot commented Mar 11, 2023

The Miri subtree was changed

cc @rust-lang/miri

@bors
Copy link
Contributor

bors commented Mar 13, 2023

☔ The latest upstream changes (presumably #108872) made this pull request unmergeable. Please resolve the merge conflicts.

@michaelwoerister
Copy link
Member

I know too little about this area.
r? types

@rustbot rustbot assigned lcnr and unassigned michaelwoerister Mar 14, 2023
@bors
Copy link
Contributor

bors commented Mar 16, 2023

☔ The latest upstream changes (presumably #107270) made this pull request unmergeable. Please resolve the merge conflicts.

@tmandry
Copy link
Member

tmandry commented Mar 21, 2023

The lang team discussed this in our meeting (notes). We didn't see any obvious concerns but want to understand more about what this change does that we and/or the types team should be looking closely at.

(Is it just the fact that the compiler accepts more programs now that it didn't before?)

Copy link
Contributor

@lcnr lcnr left a comment

Choose a reason for hiding this comment

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

some questions and nits, apart from that r=me on the impl 🤔

Comment on lines 253 to 254
let res = self.typeck_results.borrow().qpath_res(qpath, callee.hir_id);
!matches!(res, Res::Def(DefKind::Ctor(..), _))
Copy link
Contributor

Choose a reason for hiding this comment

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

i am a bit confused why that doesn't warn 🤔

Constructors are carved out on purpose, to avoid an "unreachable call" diagnostic on them, which is not very useful as they are inert.

I don't understand that comment 🤔 I would assume the outer constructor to keep warning here?

struct Foo<T>(T);

fn foo() -> Foo<String> {
    Foo(return Foo(String::new()))
}

@@ -2960,3 +2990,41 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
}
}
}

fn expr_may_be_uninhabited(expr: &hir::Expr<'_>) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

please add a comment here. is this about which expressions can actually introduce an uninhabited type. E.g. arrays is not considered because in [return;] its the nested expression that is uninhabited 🤔 but then why do we return false for ExprKind::Continue and ExprKind::Return?

@@ -470,6 +470,7 @@ impl Constructor {
(IntRange(self_range), IntRange(other_range)) => self_range.is_covered_by(other_range),
(FloatRange(void), FloatRange(..)) => match *void {},
(Str(void), Str(..)) => match *void {},
#[allow(unreachable_code)]
Copy link
Contributor

Choose a reason for hiding this comment

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

why not remove that branch in this case 😅

Dlsym::FreeBsd(dlsym) =>
freebsd::EvalContextExt::call_dlsym(this, dlsym, args, dest, ret),
#[allow(unreachable_code)]
Dlsym::Linux(dlsym) => linux::EvalContextExt::call_dlsym(this, dlsym, args, dest, ret),
Copy link
Contributor

Choose a reason for hiding this comment

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

same here? do we warn for unreachable code but you still can't remove the match arm or sth?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, removing the arm gives a non-exhaustive pattern error.

Copy link
Contributor

Choose a reason for hiding this comment

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

i think it's a pretty big issue if the unreachable code lint has false positives. I guess we could instead write match Dlsym {} here. Or what's actually happening is that the fact that Dlsym is uninhabited is conceptionally private to the linux shim.

I think the fix should be to instead change Dlsym to

pub struct Dlsym(Void);

compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs Show resolved Hide resolved
@lcnr
Copy link
Contributor

lcnr commented Mar 24, 2023

i guess one issue is that by doing it eagerly during hir typeck, we're missing cases which only diverge after type inference: (playground)

struct Wrapper<T>(Option<T>);

impl<T> Wrapper<T> {
    fn something(&self) -> T {
        panic!()
    }
}

// using `FnOnce` bound to use `!` on stable.
fn infer<F: FnOnce() -> R, R>(_: Wrapper<R>) {}

pub fn with_never() {
    let x = Wrapper(None);

    if false {
        // currently doesn't warn as liveness analysis intentionally
        // skips `!`, won't warn with this PR
        //
        // warns that `y` is unused which just seems buggy?
        let y = x.something(); 
        let _ = y; 
    }

    infer::<fn() -> !, _>(x)
}

enum Void {}
pub fn with_void() {
    let x = Wrapper(None);

    if false {
        // currently warns that the assignment is unreachable,
        // will stop warning with this PR
        let y = x.something();
        let _ = y;
    }

    infer::<fn() -> Void, _>(x)
}

i do think that is acceptable though 🤷

@lcnr lcnr added T-types Relevant to the types team, which will review and decide on the PR/issue. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Mar 24, 2023
@lcnr
Copy link
Contributor

lcnr commented Mar 24, 2023

From what I can tell the only meaningful change is that expressions which have an uninhabited type are now also considered as diverging by typeck (instead of only being considered so for the unreachable_code lint), allowing slightly more programs to compile. Currently only expressions with type ! are considered for that.

tests/ui/uninhabited/break-diverging-value.rs and tests/ui/uninhabited/break-diverging-value-pass.rs for examples of that change

it feels pretty straightforward to me and should be the responsibility of t-types, so

@rfcbot fcp merge

@rfcbot
Copy link

rfcbot commented Mar 24, 2023

Team member @lcnr has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Mar 24, 2023
@rust-log-analyzer

This comment has been minimized.

@cjgillot cjgillot removed the T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) label Jul 14, 2023
@Nadrieril
Copy link
Member

Nadrieril commented Apr 7, 2024

i guess one issue is that by doing it eagerly during hir typeck, we're missing cases which only diverge after type inference

If I understand correctly then this PR would be a breaking change to do after the min_exhaustive_patterns stabilization (currently in FCP).

For example, this is accepted today (play):

#![feature(min_exhaustive_patterns)]
#![allow(unused)]

fn infer<F: FnOnce() -> R, R>(_: Result<bool, R>) {}

enum Void {}
fn with_void() {
    let mut x = Ok(true);
    match x {
        Ok(_) => {}
    }
    infer::<fn() -> Void, _>(x)
}

Given how rust usually behaves with inference, I think it would be consistent to not accept this. But we have to decide quickly.

@rustbot rustbot added the T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) label Apr 7, 2024
@rust-log-analyzer

This comment has been minimized.

@Nadrieril
Copy link
Member

@WaffleLapkin this may be interesting to you re: the never type

Comment on lines 15 to 21
fn wild_let() -> u32 {
let ptr: *const Void = std::ptr::null();
unsafe {
//~^ ERROR: mismatched types
let _ = *ptr;
}
}
Copy link
Member

@Nadrieril Nadrieril Apr 7, 2024

Choose a reason for hiding this comment

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

This is incorrect: we should not infer that this function diverges, because let _ = *ptr; does not cause a read. In fact let _ = *(std::ptr::null::<Void>()); is valid code that creates a null pointer and does nothing with it (or maybe we need dangling(), not sure). Typechecking should distinguish place expressions from value expressions in order to be correct.

Copy link
Member

Choose a reason for hiding this comment

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

Is this not pre-existing though? I did not have time to work on #117288 and neither anyone else as far as I'm aware. Do we have any infrastructure to distinguish places & values in hir?

Copy link
Member

Choose a reason for hiding this comment

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

Idk if that's the same issue, but we shouldn't regress behavior.

Place vs value is just a matter of matching on ExprKind, there's probably a helper somewhere to distinguish the two

Copy link
Member

Choose a reason for hiding this comment

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

So what you are saying is that an expression is a place iff it's Unary(Deref, _), Index(..) or Field(..)? But are those always places? Or does it not matter because in cases where we care they are places?...

Copy link
Member

Choose a reason for hiding this comment

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

Please use MemCategorizationContext, I think that's the right helper for this

Copy link
Member

Choose a reason for hiding this comment

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

There is

/// Whether this is a place expression.
///
/// `allow_projections_from` should return `true` if indexing a field or index expression based
/// on the given expression should be considered a place expression.
pub fn is_place_expr(&self, mut allow_projections_from: impl FnMut(&Self) -> bool) -> bool {
but yeah, MemCategorizationContext looks right for this, thanks for the pointer!

Comment on lines 137 to 139
match secretely_void {
_ => {} //~ ERROR unreachable
_ => {}
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm confused, how come this "unreachable" is removed but functions like option_never2 above still compile? Can you try running just this on its own:

fn option_never(x: Void) -> impl Copy {
    if false {
        match option_never(x) {
            None => {}
        }
    }
    Some(x)
}

This should error as non-exhaustive, right?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I know: when there are no explicit patterns like in match secretely_void { _ => {} }, exhaustiveness calls inhabited_predicate on the whole type. When there are patterns, it calls it on the subtypes after normalizing. So in my option_never example it sees that Some contains Void.

In other words, this makes exhaustiveness inconsistent:

let secretely_void = SecretelyVoid(opaque_void);
match secretely_void {
    SecretelyVoid(_) => {} // WARN: unreachable
}
match secretely_void {} // ERROR: non_exhaustive

Copy link
Member

@Nadrieril Nadrieril Apr 7, 2024

Choose a reason for hiding this comment

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

I think the simple thing to do is keep the old normalizing behavior when calling is_uninhabited for exhaustiveness checking. This also solves the breaking change issue with min_exhaustive_patterns.

Comment on lines 6 to 8
pub const C: () = {
pub const C: () = { //~ ERROR evaluation of constant value failed
let E::A(ref a) = unsafe { &(&U { u: () }).e};
};
Copy link
Member

Choose a reason for hiding this comment

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

Same remark regarding places vs expressions: there is no value of type E created here, so I don't think there should be any unreachable code reached.

Comment on lines 57 to 59
if !span.is_desugaring(DesugaringKind::CondTemporary)
&& !span.is_desugaring(DesugaringKind::Async)
&& !orig_span.is_desugaring(DesugaringKind::Await)
Copy link
Member

@Nadrieril Nadrieril Apr 7, 2024

Choose a reason for hiding this comment

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

There was a distinction between span and orig_span that has been lost, was that intentional?

@@ -0,0 +1,26 @@
// Verify that we do not warn on type-dependent constructors (`Self::A` below).
Copy link
Member

@Nadrieril Nadrieril Apr 7, 2024

Choose a reason for hiding this comment

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

This comment no longer makes sense now that you warn for constructors

@bors
Copy link
Contributor

bors commented Apr 8, 2024

☔ The latest upstream changes (presumably #123569) made this pull request unmergeable. Please resolve the merge conflicts.

@WaffleLapkin
Copy link
Member

@Nadrieril thanks for the ping <3

I'm actually proposing to remove diverging block special casing in the next edition: #123590. And I don't really see how semantic changes in this PR are useful. I do not think this is a good idea.

The lint changes are probably positive though, can we change the lint without changing semantics?

@rust-log-analyzer

This comment has been minimized.

@lcnr
Copy link
Contributor

lcnr commented Aug 14, 2024

was the stabilization of feature(min_exhaustive_patterns) enough to unblock this PR?

in case it's difficult to find my new comment

@lcnr
Copy link
Contributor

lcnr commented Aug 14, 2024

there has also been some discussion between @WaffleLapkin and @Nadrieril here on whether this approach is desirable I think? May also be good for one of you two to take the review here

@WaffleLapkin
Copy link
Member

So the change proposed in #123590 did not work out, because language team didn't feel that the simpler semantics justify the breakage (esp in the light that there are code patterns that become more awkward). The sentiment generally being "if we would create a new language now: yes. but given the state of rust, probably not".

Still, I'm opposed to adding more such special cases. I don't think that the code example in the PR description should compile:

enum Void {}

fn get_void() -> Void {
    panic!()
}

fn loop_break_void() -> i32 {
    let loop_value = loop { break get_void() };
}

(block ends in a statement, why do we want to allow this?)

As I already said in #108993 (comment) changing the lint seems like a good idea. But changing the language semantics doesn't sound good.

@Nadrieril
Copy link
Member

I don't recall all the details of this here PR, but I do remember reaching the conclusion that "exhaustiveness checking must not change as a result of this PR". Also it seems that unreachable_code would trigger in ways that resemble the unreachable_patterns fallback I've been causing, which sounds equally undesirable.

@cjgillot cjgillot marked this pull request as draft August 15, 2024 01:59
@cjgillot
Copy link
Contributor Author

Haven't accounted for all the comments yet.

@rust-log-analyzer

This comment has been minimized.

@bors
Copy link
Contributor

bors commented Aug 20, 2024

☔ The latest upstream changes (presumably #122551) made this pull request unmergeable. Please resolve the merge conflicts.

Avoid deriving PartialOrd on Diverges since it includes fields which
should not affect ordering.
@rust-log-analyzer
Copy link
Collaborator

The job mingw-check-tidy failed! Check out the build log: (web) (plain)

Click to see the possible cause of the failure (guessed by this bot)

COPY host-x86_64/mingw-check/validate-toolstate.sh /scripts/
COPY host-x86_64/mingw-check/validate-error-codes.sh /scripts/

# NOTE: intentionally uses python2 for x.py so we can test it still works.
# validate-toolstate only runs in our CI, so it's ok for it to only support python3.
ENV SCRIPT TIDY_PRINT_DIFF=1 python2.7 ../x.py test \
           --stage 0 src/tools/tidy tidyselftest --extra-checks=py:lint,cpp:fmt
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
#    pip-compile --allow-unsafe --generate-hashes reuse-requirements.in
---
#13 2.744 Building wheels for collected packages: reuse
#13 2.745   Building wheel for reuse (pyproject.toml): started
#13 2.987   Building wheel for reuse (pyproject.toml): finished with status 'done'
#13 2.988   Created wheel for reuse: filename=reuse-4.0.3-cp310-cp310-manylinux_2_35_x86_64.whl size=132720 sha256=026f3bb0f1aa8090b861fd0a0939cb1a782396d84c8aab7875096557d637a0f6
#13 2.988   Stored in directory: /tmp/pip-ephem-wheel-cache-vztkzky4/wheels/3d/8d/0a/e0fc6aba4494b28a967ab5eaf951c121d9c677958714e34532
#13 2.991 Installing collected packages: boolean-py, binaryornot, tomlkit, reuse, python-debian, markupsafe, license-expression, jinja2, chardet, attrs
#13 3.385 Successfully installed attrs-23.2.0 binaryornot-0.4.4 boolean-py-4.0 chardet-5.2.0 jinja2-3.1.4 license-expression-30.3.0 markupsafe-2.1.5 python-debian-0.1.49 reuse-4.0.3 tomlkit-0.13.0
#13 3.386 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
#13 3.904 Collecting virtualenv
#13 3.904 Collecting virtualenv
#13 3.943   Downloading virtualenv-20.27.1-py3-none-any.whl (3.1 MB)
#13 3.999      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.1/3.1 MB 58.2 MB/s eta 0:00:00
#13 4.055 Collecting filelock<4,>=3.12.2
#13 4.059   Downloading filelock-3.16.1-py3-none-any.whl (16 kB)
#13 4.077 Collecting distlib<1,>=0.3.7
#13 4.081   Downloading distlib-0.3.9-py2.py3-none-any.whl (468 kB)
#13 4.089      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 469.0/469.0 KB 83.0 MB/s eta 0:00:00
#13 4.121 Collecting platformdirs<5,>=3.9.1
#13 4.126   Downloading platformdirs-4.3.6-py3-none-any.whl (18 kB)
#13 4.205 Installing collected packages: distlib, platformdirs, filelock, virtualenv
#13 4.378 Successfully installed distlib-0.3.9 filelock-3.16.1 platformdirs-4.3.6 virtualenv-20.27.1
#13 DONE 4.5s

#14 [7/8] COPY host-x86_64/mingw-check/validate-toolstate.sh /scripts/
#14 DONE 0.0s
---
DirectMap4k:      212928 kB
DirectMap2M:     8175616 kB
DirectMap1G:    10485760 kB
##[endgroup]
Executing TIDY_PRINT_DIFF=1 python2.7 ../x.py test            --stage 0 src/tools/tidy tidyselftest --extra-checks=py:lint,cpp:fmt
+ TIDY_PRINT_DIFF=1 python2.7 ../x.py test --stage 0 src/tools/tidy tidyselftest --extra-checks=py:lint,cpp:fmt
    Finished `dev` profile [unoptimized] target(s) in 0.05s
##[endgroup]
downloading https://static.rust-lang.org/dist/2024-10-16/rustfmt-nightly-x86_64-unknown-linux-gnu.tar.xz
extracting /checkout/obj/build/cache/2024-10-16/rustfmt-nightly-x86_64-unknown-linux-gnu.tar.xz to /checkout/obj/build/x86_64-unknown-linux-gnu/rustfmt
---
##[endgroup]
fmt check
fmt: checked 5641 files
tidy check
tidy error: following path contains more than 1672 entries, you should move the test to some relevant subdirectory (current: 1673): /checkout/tests/ui/issues
removing old virtual environment
removing old virtual environment
creating virtual environment at '/checkout/obj/build/venv' using 'python3.10'
Requirement already satisfied: pip in ./build/venv/lib/python3.10/site-packages (24.3.1)
All checks passed!
checking C++ file formatting
some tidy checks failed
some tidy checks failed
Command has failed. Rerun with -v to see more details.
  local time: Wed Oct 30 17:28:11 UTC 2024
  network time: Wed, 30 Oct 2024 17:28:11 GMT
##[error]Process completed with exit code 1.
Post job cleanup.

@bors
Copy link
Contributor

bors commented Nov 4, 2024

☔ The latest upstream changes (presumably #132581) made this pull request unmergeable. Please resolve the merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-testsuite Area: The testsuite used to check the correctness of rustc S-blocked Status: Marked as blocked ❌ on something else such as an RFC or other implementation work. T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.