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

Tell LLVM about impossible niche tags #139098

Merged
merged 3 commits into from
Apr 8, 2025

Conversation

scottmcm
Copy link
Member

@scottmcm scottmcm commented Mar 29, 2025

I was trying to find a better way of emitting discriminant calculations, but sadly had no luck.

So here's a fairly small PR with the bits that did seem worth bothering:

  1. As the TagEncoding::Niche docs describe, it's possible to end up with a dead value in the input that's not already communicated via the range parameter attribute nor the range load metadata attribute. So this adds an llvm.assume in non-debug mode to tell LLVM about that. (That way it can tell that the sides of the select have disjoint possible values.)

  2. I'd written a bunch more tests, or at least made them parameterized, in the process of trying things out, so this checks in those tests to hopefully help future people not trip on the same weird edge cases, like when the tag type is i8 but yet there's still a variant index and discriminant of 258 which doesn't fit in that tag type because the enum is really weird.

@rustbot
Copy link
Collaborator

rustbot commented Mar 29, 2025

r? @compiler-errors

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

@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 29, 2025
@rustbot
Copy link
Collaborator

rustbot commented Mar 29, 2025

Some changes occurred in compiler/rustc_codegen_ssa

cc @WaffleLapkin

@scottmcm scottmcm force-pushed the assert-impossible-tags branch from c325ff6 to 98e9cb2 Compare March 29, 2025 07:12
Comment on lines 165 to 181
// CHECK-LABEL: define{{.+}}i1 @match4_is_c(i8{{.+}}%e)
// CHECK-NEXT: start
// CHECK-NEXT: %[[REL_VAR:.+]] = add nsw i8 %e, -2
// CHECK-NEXT: %[[NOT_NICHE:.+]] = icmp ugt i8 %[[REL_VAR]], 4
// CHECK-NEXT: %[[NOT_IMPOSSIBLE:.+]] = icmp ne i8 %[[REL_VAR]], 2
// CHECK-NEXT: call void @llvm.assume(i1 %[[NOT_IMPOSSIBLE]])
// CHECK-NEXT: ret i1 %[[NOT_NICHE]]
#[no_mangle]
pub fn match4_is_c(e: MiddleNiche) -> bool {
// Before #139098, this couldn't optimize out the `select` because it looked
// like it was possible for a `2` to be produced on both sides.

std::intrinsics::discriminant_value(&e) == 2
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Compare on nightly, https://rust.godbolt.org/z/4bdKKjeG5

define noundef zeroext i1 @match4_is_c(i8 noundef range(i8 0, 7) %e) unnamed_addr {
start:
  %0 = add nsw i8 %e, -2
  %1 = icmp ugt i8 %0, 4
  %_01 = icmp eq i8 %0, 2
  %_0 = or i1 %1, %_01
  ret i1 %_0
}

@rust-log-analyzer

This comment has been minimized.

@compiler-errors
Copy link
Member

r? compiler

@fee1-dead
Copy link
Member

r? compiler

@rustbot rustbot assigned Nadrieril and unassigned fee1-dead Apr 4, 2025
@Nadrieril
Copy link
Member

r? codegen

@rustbot rustbot assigned workingjubilee and unassigned Nadrieril Apr 4, 2025
@bors
Copy link
Collaborator

bors commented Apr 6, 2025

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

@scottmcm scottmcm force-pushed the assert-impossible-tags branch from faaaeb4 to 51e67e2 Compare April 6, 2025 03:01
Copy link
Member

@WaffleLapkin WaffleLapkin left a comment

Choose a reason for hiding this comment

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

r? WaffleLapkin
I have a bunch of unimportant questions, but the PR looks good to me; r=me

// CHECK-NEXT: %2 = and i8 %0, 1
// CHECK-NEXT: %{{.+}} = select i1 %1, i8 13, i8 %2
// CHECK-NEXT: %[[IS_B:.+]] = icmp eq i8 %0, 2
// CHECK-NEXT: %[[TRUNC:.+]] = and i8 %0, 1
Copy link
Member

Choose a reason for hiding this comment

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

question/unrelated to this PR: why do we need this truncation? it is used in not-B case, so %0 is necesserily 0..=1...

Copy link
Member Author

Choose a reason for hiding this comment

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

You're 100% right, we don't need it.

I went to check what's up with it, and it looks like it's an opt-level thing, actually. If you use opt-level=2 it goes away: https://rust.godbolt.org/z/8Pfc99M77.

My guess would be that it's somehow related to llvm/llvm-project#132678 -- really, I wanted to change the x != 2 check to a x <= 1 check in this PR, as a better fit for looking for the natural untagged value rather than the arbitrary choice of 2 for Enum0::B, but everything like that I tried ended up regressing something :(

Copy link
Member Author

Choose a reason for hiding this comment

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

Specifically, it's CorrelatedValuePropagationPass that figures this out (it's the thing that looks at ranges and comparisons and figures out which values are possible) and that doesn't run in opt-level=1.

Copy link
Member

Choose a reason for hiding this comment

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

Interesting. But where is it even coming from in the first place? Do i1 loads always truncate?

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 coming from this https://rust.godbolt.org/z/Knr5jdEY5 InstCombine:

-  %b = trunc nuw i8 %0 to i1
-  %3 = zext i1 %b to i8
+  %2 = and i8 %0, 1

Which would be correct if it weren't for the nuw, since truncating down to one bit then zero-extending back to the original width is the same as & 1.

Filed llvm/llvm-project#134908 🙂

Copy link
Member

Choose a reason for hiding this comment

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

oof, the regressions are :(

but I'm more so asking why are we even emitting trunc in the first place.

Original llir for the some branch looks like this:

bb3:                                              ; preds = %start
  %4 = load i8, ptr %e, align 1
  %b = trunc nuw i8 %4 to i1
  %5 = zext i1 %b to i8
  store i8 %5, ptr %b.dbg.spill, align 1
  %6 = icmp ule i1 %b, true
  call void @llvm.assume(i1 %6)
  %7 = zext i1 %b to i8
  store i8 %7, ptr %_0, align 1
  br label %bb4

Is this just "we use i1 as the type for 'locals' and i8 for memory"? But then the question is why can't we use i1 for memory too? I found the answer ig:

When loading a value of a type like i20 with a size that is not an integral number of bytes, the result is undefined if the value was not originally written using a store of the same type.

Comment on lines +183 to +184
// You have to do something pretty obnoxious to get a variant index that doesn't
// fit in the tag size, but it's possible
Copy link
Member

Choose a reason for hiding this comment

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

Why the hell does E0732 exist D:

This should have really been just

enum HugeVariantIndex {
    Possible257 = 257,
    Bool258(bool),
    Possible259,
}

(nothing to do in this PR, but I'm really weirded out by the fact that that's an error)

Copy link
Member Author

Choose a reason for hiding this comment

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

The current Niche encoding in rustc_abi can't actually represent that -- the way it's specified it only accepts things with discriminant equal the variant index -- so even if it was legal we'd still have to do the other thing.

But I think the reason is that there's no stable way to actually read that discriminant, so it's of questionable value to actually bother having it legal.

Copy link
Member

Choose a reason for hiding this comment

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

Fun!

#[no_mangle]
pub fn option_nonzero_match(x: Option<std::num::NonZero<u16>>) -> u16 {
// CHECK: %[[IS_NONE:.+]] = icmp eq i16 %x, 0
// CHECK: %[[OPT_DISCR:.+]] = select i1 %[[IS_NONE]], i64 0, i64 1
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to not emit a select if it's equivalent to zext? Or would that be pointless?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know if it's really worth it vs just letting LLVM do it for us.

(And this one would need to flip the icmp eq to icmp ne in order to use a zext, which I think is actually always the case for a niche-encoded Option. So maybe it'd be worth flipping the way we emit things so that we could then use the zext, but it's not something I'd bother doing in this PR. Really the most silly part is that we need to widen to isize only to trunc down to bool anyway, but those come from different parts of the MIR so it's hard to fix without adding new MIR things that probably aren't worth it, so I'm inclined to just stick with the "meh, LLVM will handle it" approach.)

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I originally thought about widen/trunc too, but figured that this is just multiple parts of our code assuming the type.

@scottmcm
Copy link
Member Author

scottmcm commented Apr 8, 2025

@bors r=WaffleLapkin

@bors
Copy link
Collaborator

bors commented Apr 8, 2025

📌 Commit 502f7f9 has been approved by WaffleLapkin

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 8, 2025
Zalathar added a commit to Zalathar/rust that referenced this pull request Apr 8, 2025
…=WaffleLapkin

Tell LLVM about impossible niche tags

I was trying to find a better way of emitting discriminant calculations, but sadly had no luck.

So here's a fairly small PR with the bits that did seem worth bothering:

1. As the [`TagEncoding::Niche` docs](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_abi/enum.TagEncoding.html#variant.Niche) describe, it's possible to end up with a dead value in the input that's not already communicated via the range parameter attribute nor the range load metadata attribute.  So this adds an `llvm.assume` in non-debug mode to tell LLVM about that.  (That way it can tell that the sides of the `select` have disjoint possible values.)

2. I'd written a bunch more tests, or at least made them parameterized, in the process of trying things out, so this checks in those tests to hopefully help future people not trip on the same weird edge cases, like when the tag type is `i8` but yet there's still a variant index and discriminant of `258` which doesn't fit in that tag type because the enum is really weird.
bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 8, 2025
…errors

Rollup of 19 pull requests

Successful merges:

 - rust-lang#138676 (Implement overflow for infinite implied lifetime bounds)
 - rust-lang#139024 (Make error message for missing fields with `..` and without `..` more consistent)
 - rust-lang#139098 (Tell LLVM about impossible niche tags)
 - rust-lang#139124 (compiler: report error when trait object type param reference self)
 - rust-lang#139321 (Update to new rinja version (askama))
 - rust-lang#139346 (Don't construct preds w escaping bound vars in `diagnostic_hir_wf_check`)
 - rust-lang#139386 (make it possible to use stage0 libtest on compiletest)
 - rust-lang#139421 (Fix trait upcasting to dyn type with no principal when there are projections)
 - rust-lang#139468 (Don't call `Span::with_parent` on the good path in `has_stashed_diagnostic`)
 - rust-lang#139476 (rm `RegionInferenceContext::var_infos`)
 - rust-lang#139481 (Add job summary links to post-merge report)
 - rust-lang#139485 (compiletest: Stricter parsing for diagnostic kinds)
 - rust-lang#139490 (Update some comment/docs related to "extern intrinsic" removal)
 - rust-lang#139491 (Update books)
 - rust-lang#139496 (Revert r-a changes of rust-lang#139455)
 - rust-lang#139500 (document panic behavior of Vec::resize and Vec::resize_with)
 - rust-lang#139501 (Fix stack overflow in exhaustiveness due to recursive HIR opaque hidden types)
 - rust-lang#139504 (add missing word in doc comment)
 - rust-lang#139507 (compiletest: Trim whitespace from environment variable names)

r? `@ghost`
`@rustbot` modify labels: rollup
bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 8, 2025
Rollup of 10 pull requests

Successful merges:

 - rust-lang#138676 (Implement overflow for infinite implied lifetime bounds)
 - rust-lang#139024 (Make error message for missing fields with `..` and without `..` more consistent)
 - rust-lang#139098 (Tell LLVM about impossible niche tags)
 - rust-lang#139124 (compiler: report error when trait object type param reference self)
 - rust-lang#139321 (Update to new rinja version (askama))
 - rust-lang#139346 (Don't construct preds w escaping bound vars in `diagnostic_hir_wf_check`)
 - rust-lang#139386 (make it possible to use stage0 libtest on compiletest)
 - rust-lang#139421 (Fix trait upcasting to dyn type with no principal when there are projections)
 - rust-lang#139464 (Allow for reparsing failure when reparsing a pasted metavar.)
 - rust-lang#139490 (Update some comment/docs related to "extern intrinsic" removal)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit 7ffa56c into rust-lang:master Apr 8, 2025
6 checks passed
@rustbot rustbot added this to the 1.88.0 milestone Apr 8, 2025
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Apr 8, 2025
Rollup merge of rust-lang#139098 - scottmcm:assert-impossible-tags, r=WaffleLapkin

Tell LLVM about impossible niche tags

I was trying to find a better way of emitting discriminant calculations, but sadly had no luck.

So here's a fairly small PR with the bits that did seem worth bothering:

1. As the [`TagEncoding::Niche` docs](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_abi/enum.TagEncoding.html#variant.Niche) describe, it's possible to end up with a dead value in the input that's not already communicated via the range parameter attribute nor the range load metadata attribute.  So this adds an `llvm.assume` in non-debug mode to tell LLVM about that.  (That way it can tell that the sides of the `select` have disjoint possible values.)

2. I'd written a bunch more tests, or at least made them parameterized, in the process of trying things out, so this checks in those tests to hopefully help future people not trip on the same weird edge cases, like when the tag type is `i8` but yet there's still a variant index and discriminant of `258` which doesn't fit in that tag type because the enum is really weird.
@scottmcm scottmcm deleted the assert-impossible-tags branch April 8, 2025 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants