-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Functions with uninhabited return values codegen trap instead of unreachable #59793
Comments
Note also that we use |
@nikic we use
|
We can change the I'm not certain that we should though, since your |
One note about going to just unreachable -- here's your debug IR: ; issue59793::empty
; Function Attrs: noreturn nonlazybind uwtable
define void @_ZN10issue597935empty17h4ff3ba3a79e34872E(%EmptyEnum* noalias nonnull readonly align 1) unnamed_addr #1 !dbg !13 {
start:
%x = alloca %EmptyEnum*, align 8
store %EmptyEnum* %0, %EmptyEnum** %x, align 8
call void @llvm.dbg.declare(metadata %EmptyEnum** %x, metadata !22, metadata !DIExpression()), !dbg !23
call void @llvm.trap(), !dbg !24
unreachable, !dbg !24
}
; issue59793::unreach
; Function Attrs: noreturn nonlazybind uwtable
define void @_ZN10issue597937unreach17h2cd2e01f0a9a6888E(%EmptyEnum* noalias nonnull readonly align 1) unnamed_addr #1 !dbg !25 {
start:
%x = alloca %EmptyEnum*, align 8
store %EmptyEnum* %0, %EmptyEnum** %x, align 8
call void @llvm.dbg.declare(metadata %EmptyEnum** %x, metadata !26, metadata !DIExpression()), !dbg !27
; call core::hint::unreachable_unchecked
call void @_ZN4core4hint21unreachable_unchecked17h592d1c4b48415e36E(), !dbg !28
unreachable, !dbg !28
} If I comment out the trap,
This doesn't happen without debuginfo, but think of the |
It seems to me that this is probably a good place to signal UB / I think it would be more interesting to find bad codegen which didn't do anything dubious like this. For the examples I tested in #59639 (comment), the code which might deal with uninhabited values is truly dead, as they come from using an infallible |
Sure, that sounds about right? Just like reference types provide a safe way to write an otherwise unsafe construct (pointer dereference), we can do similar things based on uninhabited return types. We do agree that it would be correct to put an
That's some overeager linting right there. :/ Seems like LLVM wants an analysis that "goes backward" and erases unreachable code. I think LLVM is just being silly here though.
Well, this is where I'd advise caution for now. ;) We never do a pointer read here, we just create a reference to an uninhabited type. Whether such a reference (when never used, or only used to create a place but never to create an rvalue) is insta-UB is an open question, part of which is discussed at rust-lang/unsafe-code-guidelines#77. Cc @rust-lang/wg-unsafe-code-guidelines |
But unlike pointer dereference, this is an unsafe construct that's always UB.
If I may continue hedging, I guess I do agree that it's semantically correct. We know at a high level that these values can't exist, so it's only "reachable" by misbehaving
I see the point. If we were talking about something like But I'm aware we do actually create allocations to allow partial initialization, per #49298, so... 🤷♂️ |
This is mirrored by the fact that the safe code we are talking about is never executed. I don't see an issue. We do codegen for safe code all the time under the assumption that all surrounding unsafe code behaves appropriately. We add
"semantically" as opposed to... what? Returning from |
That's somewhat circular. It's only never executed if it's pruned as dead code, which we want the optimizer to do on the basis of UB. In debug mode, there is a real path where this could be executed. I'd prefer it if we had an example that wasn't obviously doing something wrong. I'll see if I can figure out how to match any of the recent increase in compiler
I mean that it's not supposed to be possible to create an uninhabited value, so we want to assume it never happens. But if you called If this were real code where we could wave it off and say that we know these are never called with |
It doesn't affect what is correct, but it affects ergonomics. To the extent deciding how strictly to exploit UB is a tradeoff between performance and developer ergonomics, it's helpful to know that LLVM will trap rather than literally emitting no instructions and falling through into whatever chunk of code comes next, something that both GCC and Clang do by default on Linux :) |
No, it is never executed period -- because only UB programs can ever get there, and we do not consider UB programs. There is no circularity.
That path is just as real as the path to an
I don't understand?
This is exactly what UB is for: the compiler may assume it does not happen, and the programmer writing
LLVM might trap. Or it might do anything else. That's what I mean by "unreliable lint". See |
I assumed #59639 was about generic functions when codegen ends up seeing a return statement but the choice of type parameters made it have an uninhabited return type. |
Might be related: the asm for the playground link in the OP reveals pointless
|
OK, fine, maybe I'm trying to be too nice to UB.
Your example is directly creating UB in hopes of seeing it optimized away. That's somewhat useful as a minimal test, but it's not a realistic situation in itself. If someone actually had that code and complained about codegen, I'd say they should just prune that unreachable branch themselves. It would be more compelling to see realistic code that leads to a situation like that, probably using generic types as @eddyb suggests. Something where the code is entirely reasonable for some types, but with uninhabited types would have dead code that we want pruned. If a trap survives here, that's something to care about. |
That's fair. It's just much harder to come up with that.^^ |
That seems to be it. I just tried comparing compiler builds of the exact same source, only with and without the trap, and all 3 new @@ -82417,9 +82417,10 @@ Disassembly of section .text:
000000000008ea80 <<libc::unix::notbsd::timezone as core::clone::Clone>::clone>:
8ea80: 0f 0b ud2
- 8ea82: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
- 8ea89: 00 00 00
- 8ea8c: 0f 1f 40 00 nopl 0x0(%rax)
+ 8ea82: 0f 0b ud2
+ 8ea84: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
+ 8ea8b: 00 00 00
+ 8ea8e: 66 90 xchg %ax,%ax
000000000008ea90 <libc::unix::notbsd::CMSG_ALIGN>:
8ea90: 48 8d 47 07 lea 0x7(%rdi),%rax
@@ -83112,9 +83113,10 @@ Disassembly of section .text:
000000000008f280 <<libc::unix::notbsd::linux::fpos64_t as core::clone::Clone>::clone>:
8f280: 0f 0b ud2
- 8f282: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
- 8f289: 00 00 00
- 8f28c: 0f 1f 40 00 nopl 0x0(%rax)
+ 8f282: 0f 0b ud2
+ 8f284: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
+ 8f28b: 00 00 00
+ 8f28e: 66 90 xchg %ax,%ax
000000000008f290 <<libc::unix::notbsd::linux::rlimit64 as core::clone::Clone>::clone>:
8f290: 48 8b 07 mov (%rdi),%rax
@@ -83517,9 +83519,10 @@ Disassembly of section .text:
000000000008f6f0 <<libc::unix::DIR as core::clone::Clone>::clone>:
8f6f0: 0f 0b ud2
- 8f6f2: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
- 8f6f9: 00 00 00
- 8f6fc: 0f 1f 40 00 nopl 0x0(%rax)
+ 8f6f2: 0f 0b ud2
+ 8f6f4: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
+ 8f6fb: 00 00 00
+ 8f6fe: 66 90 xchg %ax,%ax
000000000008f700 <<libc::unix::rusage as core::clone::Clone>::clone>:
8f700: 53 push %rbx The other changed instructions are just different sized NOPs for function alignment. All 3 of those types are empty enums. It's not clear to me why they should implement The same 3 appear in a couple other libraries, but otherwise I see no effect at all on the entire compiler, having the trap or just a bare void Lint::visitUnreachableInst(UnreachableInst &I) {
// This isn't undefined behavior, it's merely suspicious.
Assert(&I == &I.getParent()->front() ||
std::prev(I.getIterator())->mayHaveSideEffects(),
"Unusual: unreachable immediately preceded by instruction without "
"side effects",
&I);
} |
One lint avoidance is to |
(I know. My wording could be improved, I was trying to say it would trap in cases where it would otherwise do the specific other thing I mentioned.) |
This recently came up again in #67054. |
This still reproduces on today's nightly. |
With #59639, I would expect the following two functions to generate the same LLVM IR:
However, they do not:
Namely,
empty
triggers a well-defined trap whileunreach
causes UB.That this makes a difference can be seen when adding:
The first becomes
but the second becomes
because the second branch does not actually cause UB.
Cc @eddyb @cuviper
The text was updated successfully, but these errors were encountered: