41
$\begingroup$

In Java, the following is a compile-time error, due to the unreachable statement:

while(true) {
    break;
    System.out.println("unreachable");
}

In Rust, the equivalent code is not:

loop {
    break;
    println!("unreachable");
}

In general, determining reachability is undecidable; however, that doesn't stop compilers from applying rules which can statically detect some but not all unreachable code. The Rust compiler already has logic for this, in order to emit a warning (and also to discard the unreachable code), but the same logic is not used to emit an error message. So in Rust and likely many other cases, this is a design decision, not a feasibility issue.

What reasons have the designers of languages given for or against emitting errors for unreachable code? I'm particularly interested in the rationales for mainstream languages which either do make this an error, or which don't but have plenty of other static checks; authoritative answers about other languages are also welcome.

$\endgroup$
10
  • 14
    $\begingroup$ Languages that expect to be used as a target for code generators generally don't want to flag errors about things that are merely inefficient or odd and not definitely wrong. $\endgroup$
    – Chris Dodd
    Commented Mar 21 at 22:17
  • 2
    $\begingroup$ Some languages (such as ABB's RAPID) are designed to run in the hardware space as well as the software space. In such cases, unreachable code CAN be executed by manually shifting the point of execution. Using ABB as the example, the industrial robots may have a set of motions locked behind and IF(FALSE) to prevent automatic running, but allow a programmer to manually execute that path. As a programmer who occupies both programming worlds, I can see the merits of having unreachable code, even in PROD-ENV in a pure software system. This is becoming increasingly prevalent due to MC's and Python. $\endgroup$
    – GOATNine
    Commented Mar 22 at 20:51
  • 1
    $\begingroup$ @GOATNine "manually shifting the point of execution" is equivalent to a GOTO, or at least a loader or debugger executing at an arbitrary location. $\endgroup$
    – smci
    Commented Mar 23 at 7:25
  • 2
    $\begingroup$ Also, IIRC one of the side-effects of removing code that is only reachable in the debug case is it can cause associated memory allocation, strings etc. to move around a lot (unwantedly), which in turn can make bugs (buffer overflows, off-by-one errors, etc.) only get tickled in runtime but not debug-mode build, since debug build adds a lot of pessimism and padding. That might not be desirable. $\endgroup$
    – smci
    Commented Mar 23 at 8:25
  • 3
    $\begingroup$ There are lots of answers now on why language X doesn't error on unreachable code. Will nobody write an answer that explains why Jave does? $\endgroup$
    – James K
    Commented Mar 24 at 20:24

7 Answers 7

61
$\begingroup$

Great question. I can tell you a bit about C# and why in C#, unreachable code is a warning, not an error. I'll start by briefly describing how reachability detection works, then what it is used for, and then why it's a warning, not an error.

Unreachable code is detected by a few relatively simple rules. The simplest are ones like the case you describe; we can very easily partition statements into "blocks" where control enters at the top of the block and does not leave normally (that is, without an exception) until the end of the block. If no control flow enters the top of a basic block, the whole block is unreachable.

Those statements that always "go somewhere else" (goto, return, break, continue, throw) are said to have an "unreachable endpoint". As does any statement that is itself unreachable.

Next, C# does an analysis of compile-time constant expressions; these are expressions involving only literals or named constants. The else block of an if(0==0) condition is unreachable, but C# would consider the else block of if(x * 0 == 0) for integer variable x to be reachable, since the expression is not a constant expression. (Fun fact, C# 1 and 2 had a bug where it ran the arithmetic optimizer too early and treated some expressions of this form as constants; I fixed it for C# 3.) while(true) {} has an unreachable endpoint.

C# also analyzes the logical or/and expressions to determine reachability, which is where the rules get a little complicated; see the spec for details.

And let me finish this brief overview with the puzzle I always pose when discussing reachability. There is a C# program where there is a reachable goto, but its target label is unreachable. Can you give an example of such a program?


Why does C# care about reachability at all?

  • The end point of the last statement in a method with a non-void return type must be unreachable, otherwise we have a bug; the method doesn't necessarily return a value.

  • If the end point of the last statement in a method is reachable then every out parameter must be definitely assigned before that, otherwise we have a bug, we are possibly returning without assigning the out parameter.

  • In C and C++, the switch statement is bug prone. In a switch in C, each label starts a section, that section can be empty, and so in order to have two labels with the same consequence, control must fall through from the bottom of one section to the next. C# fixes this logical problem by instead saying that a section is never empty; it always has at least one statement, and that a section may have multiple labels. A section must have an unreachable endpoint.

  • C# disallows reading from a local variable before it is known to have been assigned, again because this is a common source of bugs. The rules that determine when a variable is known to be assigned and known to be read are based on reachability analysis. If there is a possibly-reachable read where the corresponding write is unreachable or possibly unreachable, that's an error. But a read of a possibly-unassigned local is not an error if the read itself is unreachable.

What I'm getting at here is: we care about reachability analysis because it enables automatically finding common bugs and preventing them at compile time. But why then is unreachable code not itself a bug that makes a compile-time error? And why is reading from an unassigned local legal in unreachable code?


To facilitate debugging! I'm sure you've encountered this situation of:

if(some complicated and hard to reproduce condition)
{
  there's a bug in this code
} else {
  some perfectly normal code
}

If the repro is a pain, the natural thing to do is to just write

if(true /* painful repro condition */)
{
  // put your breakpoint here
  there's a bug in this code
} else {
  some perfectly normal code
}

If that's a compile-time error because the else case is now unreachable, then you're slowing down the debugging process. If it is not a warning, then you're risking the developer accidentally checking in debug code. Therefore the sensible thing to do is to make it a warning.

Same with unassigned locals.

int x;
if (Validate(y, out x))
{
  do something with x
} else {
  uh oh the error case has a bug in it
  but it does not read x
}

We quickly debug it by

int x;
if (false /*Validate(y, out x)*/)
{
  do something with x
} else {
  // breakpoint here
  uh oh the error case has a bug in it
  but it does not read x   
}

In this debugging scenario "do something with x" reads x before it is assigned. Doesn't matter! Reading a variable before it is assigned in unreachable code is not an error. Rather, the unreachable code is a warning so that you don't check it in by mistake.

There are other scenarios as well; here's one I described in 2012.


The lesson for language designers here is to think about everything that a developer is going to do. Code is not just written and checked in; it's modified in temporary ways during debugging, often under time pressure, and anything we can do to make that process easier makes developers more productive.


If this subject interests you, I've written about it a fair amount. A good start is this now-deleted post on the Coverity blog:

https://web.archive.org/web/20140331160029/http://blog.coverity.com/2013/11/06/c-reachability#.UzmRJ4HP1qY

$\endgroup$
14
  • 6
    $\begingroup$ Also of note is that C is still widely used today, and is used prominently (maybe exclusively or almost exclusively) in bare-metal embedded development. In that case, where you read a variable, it might not even be a variable at all, but a hardware element, a sensor value, etc. therefore the compiler should not disallow you from reading an unassigned variable, because it might be assigned by hardware. Similarly, code can be executed by hardware interrupts, which the compiler might now nothing about. $\endgroup$
    – vsz
    Commented Mar 22 at 7:56
  • 18
    $\begingroup$ As a longtime C# developer, when I read the title, I was thinking "Man, I introduce unreachable code when debugging and rapid prototyping all the time!", it was really interesting to read your answer. I'm grateful to you and the team for thinking of the many man hours I have saved :-) $\endgroup$ Commented Mar 22 at 8:24
  • 1
    $\begingroup$ @vsz: This restriction on C# only applies to locals and "out" formals; fields, array elements, pointer dereferences, are assumed to be initially assigned. $\endgroup$ Commented Mar 22 at 9:15
  • 2
    $\begingroup$ @vsz when we access hardware from embedded code we do so by dereferencing a volatile pointer variable eg volatile unsigned int *. The pointer variable itself must be initialized to the correct address (alternatively we dereference a pointer literal). When we talk about uninitialized variables in C we are talking a non-static variable that is local to a block and has not been assigned yet. Such a variable will usually reside on the stack or it may be optimized into a register. Either way its contents are considered unknown and the standard says reading it is undefined behaviour. $\endgroup$
    – Rodney
    Commented Mar 22 at 9:53
  • 7
    $\begingroup$ For those stumped by the "unreachable goto target" riddle: stackoverflow.com/a/15217509/1662268 $\endgroup$
    – Brondahl
    Commented Mar 22 at 10:03
29
$\begingroup$

Because it's not uncommon to create unreachable code deliberately. Sometimes it's just temporary during incremental development, sometimes it's permanent.

For instance, you might have code like

if (DEBUG) {
    // do stuff
}

When the DEBUG macro expands to false all this code is unreachable.

Sometimes you have code like

if (condition) {
    // do stuff
}

and you want to temporarily disable it. So you change the first line to

if (false && (condition)) {

There are other ways to accomplish this, such as testing the macro in the preprocessor, or commenting out the whole block of code. But doing it like this is often preferable because the block structure of the code is preserved.

$\endgroup$
3
  • 4
    $\begingroup$ Note that Java has a special case for this due to debugging: if(false){...} is considered okay from the compiler, but while(false){...} is considered an error as unreachable code. $\endgroup$
    – Nayuki
    Commented Mar 22 at 15:10
  • 2
    $\begingroup$ @Nayuki And warnings from compilers, linters, and IDEs can also often be silenced with special comments. So my answer is not a completely compelling argument. $\endgroup$
    – Barmar
    Commented Mar 22 at 15:12
  • 1
    $\begingroup$ A decade after the "goto fail" bug and this really shouldn't be the correct answer. Production code should error on unreachable code, while deliberate debug-build-only code for things like assertions that are condition on special flags like DEBUG should be identified by the compiler as an exception. Any remaining edge cases should be up to the developer to deliberately mark as exceptions with some kind of compiler pragma. Of course probably not with C, but modern languages should be built this way from the start and the DEBUG build flag should be special. $\endgroup$
    – lamont
    Commented Mar 29 at 17:50
14
$\begingroup$

In languages that have a preprocessor, templates or macros, unreachable code often occurs without being an issue:

  • Configuration options checked with #ifdef can affect what code is run. It's not always reasonable to #ifdef out the unreachable code, as the condition could get much more complex than the individual checks.
  • Target platform affects the size of types like int, which causes unreachable branches in checks based on sizeof(int). This is usually indicative of the opposite of a bug: the code deliberately handles platform differences by checking the size.
  • C++ templates can result in unreachable code by checking the properties of types, or by numeric range limitations.
  • Compiler optimizations and inlining result in unreachable code when functions or macros are used in specific cases.

That said, many C and C++ compilers do support a warning about unreachable code, and usually also allow configuring warnings as errors.

$\endgroup$
3
  • 1
    $\begingroup$ I would argue that #if FALSE code isn't so much unreachable as non-existant from the compiler perspective. $\endgroup$ Commented Mar 22 at 15:58
  • $\begingroup$ @SoronelHaetir I agree, but if you have #if CONFIG_OPTION return do_other_thing(); #endif ... normal stuff .. the normal stuff can become unreachable. With a single config option you can always #else it, but with multiple options it gets complex quick. Though many would say that a lot of #ifdefs is a code smell to start with :) $\endgroup$
    – jpa
    Commented Mar 22 at 18:38
  • 2
    $\begingroup$ +1 Yes, this is the reason. The same reasoning applies to languages that heavily rely on inlines: it's easier to tolerate a few unreachable branches in the inlined / generated code here and there, rather than forcing the user to make their inlined method / macros super-complex in order to avoid the generation of unreachable branches. $\endgroup$ Commented Mar 23 at 14:08
4
$\begingroup$

I'll come at this from a primarily C perspective.

In general, determining reachability is undecidable; however, that doesn't stop compilers from applying rules which can statically detect some but not all unreachable code.

Many programming languages predate the existence of compilers that could do that sort of detection. Early C compilers didn't perform any real optimization at all, they just translated what's on the page directly to assembly. Forbidding unreachable code would be a rule that was impossible to enforce, thus pointless. Even in the modern era, trying to enforce this rule can result in code that compiles on one compiler and not on another. That makes things unnecessarily difficult for the programmer. This could even happen on the same compiler between two different sets of compiler options.

This becomes even more of a problem when you start trying to write code that's reusable or cross-platform. The C preprocessor allows you to #ifdef chunks of code and hide them, but that preprocessor is extremely primitive. It's not uncommon to need to gate a chunk of code using logic that can be resolved at compile time, but is much more sophisticated than what the preprocessor is capable of. Oversimplified example:

void transmit_token(token *tok) /* platform-specific typedef */
{
    queue_token(tok);
    // Receiving device expects a 32-bit transmission,
    // insert padding bytes if necessary.
    for (uint i = 0; i < (4 - sizeof(*tok)); ++i)
        queue_byte(PAD_BYTE);
    transmit_waiting_bytes();
}

When data type token is 32 bits long, the compiler can easily tell that the for loop is dead code. The preprocessor has no knowledge of types and can't evaluate a statement that complex, so if dead code was forbidden you'd have to write something like this instead:

void transmit_token(token *tok) /* platform-specific typedef */
{
    queue_token(tok);
    // Receiving device expects a 32-bit transmission,
    // insert padding bytes if necessary.
#if defined TOKEN_IS_ONE_BYTE
    queue_byte(PAD_BYTE);
    queue_byte(PAD_BYTE);
    queue_byte(PAD_BYTE);
#elif defined TOKEN_IS_TWO_BYTES
    queue_byte(PAD_BYTE);
    queue_byte(PAD_BYTE);
#elif defined TOKEN_IS_THREE_BYTES
    queue_byte(PAD_BYTE);
#endif
    transmit_waiting_bytes();
}

Gross. Not only is it far more complex and copy/paste-y, but now I have to manually define a boat load of platform-specific preprocessor constants. That introduces a lot of opportunity for human error (don't ask how I know). When porting this code to a new platform, I now have to read through the entire codebase and determine which lines need platform-specific modifications vs. having a generic version of the code that just works.

On projects that I've worked on personally, a hard rule against dead code would have prevented me from having re-usable code libraries. I would have needed to maintain separate x86, PPC, and MIPS versions of my library code, and then somehow manually keep them all in sync at all times. That quickly turns into a bug farm. Instead, I had a single library that could be built for all platforms. Some of the code was unreachable for some platforms, but that's perfectly OK. The compiler detected it and cut it out automatically.

Forbidding dead code could even have legal consequences. Let's say that you're using a third-party library and the compiler detects dead code in a library header (including a header in C essentially pastes its contents into your source file, so the compiler processes it just like the rest of your code) for your particular combination of platform, compiler, and compile options. What do you do? Modify the header to remove the dead code? A library with a proprietary license likely doesn't give you the rights to make modifications at all. If the library is using something like the LGPL, modifying it would be considered creating a derivative work and would then require you to publish your source code and do other things that may not be desirable. All just to get around a compiler error that's complaining about something that isn't actually a problem. Even if I did have the rights to modify the header, I may not have the source for the library itself. How do I know my changes aren't breaking something?

At the end of the day, dead code is really only a problem if it's unintentionally dead. The drawbacks of throwing a compiler error here far outweigh the benefits. That's not to say that C programmers don't care about dead code, we just use a static analyzer for that and not the compiler.

$\endgroup$
1
$\begingroup$

In Python, only syntax errors and warnings can happen at compile time, and unreachable code wouldn't qualify as a syntactic issue. So reachability analysis simply isn't done. It's designed like this for a few reasons:

  • It simplifies understanding of error messages, and lets one get to the unit tests faster.

  • Everything is so dynamic that it would be hard to prove (non)reachability in pathological cases, anyway. For example, a name for a global variable could be read from a file and used to update the globals() dict, subsequently impacting an if condition later in the same function.

  • Since things are so dynamic, there is at least some pressure to make compilation fast, since it happens frequently and as part of the execution of the program (rather than all ahead of time).

  • There is no pressure from correctness requirements, largely due to dynamic typing. For example, there's no problem of "if a non-void function reaches the end without reaching return then it would be a bug for the compiler to accept that" - because every function is non-void, and every function may return a value of any type, and there is a specific value dedicated for the purpose of being the return value of functions that reach the end without encountering return (i.e., None).

  • Having bytecode that closely reflects the structure of the code is considered an advantage for debugging. Very little optimization is done at the level of peephole optimization of bytecode; the optimization work is focused on the design of the virtual instruction set itself, and on the code that interprets those bytecodes. Even the "optimization" flags only strip out some metadata from the resulting .pyc files. There are some very specific hard-coded cases: e.g. code inside if False: will be optimized out, but code inside even something as trivial as if 1 == 1: will not.

$\endgroup$
3
  • $\begingroup$ Probably worth pointing out that there are many extra tools that do reachability analysis. It’s just something one opts in as desired. $\endgroup$ Commented Mar 23 at 8:48
  • 1
    $\begingroup$ These are good points, but is there any authoritative source to show that they are the rationale of the Python designers? $\endgroup$
    – kaya3
    Commented Mar 23 at 17:49
  • 1
    $\begingroup$ This is what I've understood from my own expertise. I'm sure there is some documentation for at least some of these points, but I'd have to do fresh research. $\endgroup$ Commented Mar 23 at 19:30
-1
$\begingroup$

Since unreachable code detection is undecidable, as a compiler evolves the detection algorithm can become better and/or worse. It would be a bad idea to have code that compiles successfully on one version and fails on the next simply because the compiler got smarter at detecting unreachable code.

$\endgroup$
1
  • 2
    $\begingroup$ Java's approach is to exactly specify the rules for deciding which code to error on, and not change those rules to make them error on more code. Meanwhile Typescript also errors on unreachable code, and is happy to push minor updates which report errors in programs where there previously were none, since finding errors is the main reason people use Typescript. This could be a rationale for some languages, but do you know of any where the designers have said it is? $\endgroup$
    – kaya3
    Commented Mar 23 at 17:53
-3
$\begingroup$

The only problem that would be solved by prohibiting unreachable code would be to prevent bloated executables.

That's a performance issue. Compilers aren't supposed to stop the developer from being inefficient.

$\endgroup$
4
  • $\begingroup$ Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center. $\endgroup$ Commented Mar 23 at 23:40
  • 2
    $\begingroup$ The problem with unreachable code is not that it takes up space in the executable; a good compiler will skip emitting unreachable code. The problem with unreachable code is the developer probably believes it is reachable and the developer is wrong! Prohibiting unreachable code isn't an optimization, it's a bug detector. $\endgroup$ Commented Mar 24 at 18:02
  • $\begingroup$ Compilers should not throw errors for things that are probably wrong, but for things that are certainly wrong. $\endgroup$
    – EvilSnack
    Commented Mar 24 at 18:24
  • $\begingroup$ @EvilSnack And yet Java and Typescript are two examples of mainstream languages which give compile-time errors for unreachable code. I'm sure there are languages where your rationale is the one given for making this decision, but you should identify those languages and show that this is why they designed it that way. Otherwise this is just your opinion. $\endgroup$
    – kaya3
    Commented Mar 24 at 20:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .