3
$\begingroup$

Several languages have a function or macro to indicate that a certain point in control flow cannot be reached, even though the compiler cannot statically prove it, such as unreachable!() in Rust and __builtin_unreachable() in gcc.

However, some languages don't have this. Even Swift, which borrowed many features from Rust, omits this -- you have to call fatalError (equivalent to panic!) instead. In languages with static analysis that make this relevant, why would it be beneficial to omit this function?

$\endgroup$
4
  • $\begingroup$ Didn't you mean to compare __builtin_unreachable with Rust's unreachable_unchecked? Rust's unreachable! is not equivalent to GCC's since the former panics in a defined way, while the later will lead to UB if the code path reaches it. $\endgroup$ May 18, 2023 at 3:10
  • 2
    $\begingroup$ Rust's uncreachable is equivalent to panic, as the docs say. $\endgroup$
    – kaya3
    May 18, 2023 at 3:17
  • 1
    $\begingroup$ @LuizFelipe I honestly prefer a checked function -- see my unreachable macro here, for example -- it's just that I don't know of any builtin like this in C/C++. $\endgroup$
    – Bbrk24
    May 18, 2023 at 3:25
  • $\begingroup$ Yeah, I see. I'm not sure if Rust's unreachable_unchecked panics in a well-defined manner in debug builds. As per the docs, I think it doesn't. However, unreachable! is always checked—both in debug and release builds. $\endgroup$ May 18, 2023 at 3:29

3 Answers 3

9
$\begingroup$

As you pointed out, these markers are useful when the compiler can't statically prove that the code path is unreachable. You, as the programmer, may use them when you are sure that such a code path is indeed—and certainly—unreachable. Otherwise, here be dragons!

Both GCC's __builtin_unreachable and Rust's unreachable_unchecked serve as an unsafe optimization that you, the programmer, used to eliminate some code path that will never be executed.

Why do some languages not have an 'unreachable' function?

These other languages probably shouldn't allow such kind of unsafety.

See, the compiler will erase all code paths that led to the unreachable marker, which can cause UB if the code path is reached.

Programmers can make wrong assumptions, so most languages can't afford to allow unchecked unsafe operations, even at the cost of optimization.

In the case of C and C++, the language is, by definition, "unsafe", and the programmer is responsible for ensuring the program's safety; hence, the annotation is available.

In Rust, unreachable_unchecked must be put behind an unsafe block, leaving the programmer responsible for the program's safety.

Rust's unreachable! macro is safe because it will panic (in a defined manner) if the code reaches it. Unlike unreachable_unchecked, the unreachable! macro won't trigger any optimizations by itself; it is just commonly used as a more semantically appropriate way of panicking.

$\endgroup$
3
$\begingroup$

It's not necessary

you have to call fatalError (equivalent to panic!) instead

What's the problem? Doesn't this work just fine?

Also, many languages aren't strict enough to require this - they don't care if your code is reached or not.

$\endgroup$
2
  • $\begingroup$ Without some kind of feedback, it's not clear to the compiler that fatalError or panic never returns. This may introduce warnings or even errors in some languages; consider a variable which is initialised along all reachable code paths but uninitialised along unreachable ones. The programmer ideally should not get a warning/error about an uninitialised variable in this case. $\endgroup$
    – Pseudonym
    May 18, 2023 at 5:00
  • 1
    $\begingroup$ @Pseudonym I think it should be clear, many languages have it as an actual keyword (throw/raise), so it's not just a regular macro, it's something the compiler should recognize. $\endgroup$
    – naffetS
    May 18, 2023 at 21:41
2
$\begingroup$

Mercury has a different solution. Being a logic language, it has a notion of a predicate/procedure returning multiple solutions (via backtracking) and failing. But it also has a strong determinism system, which means that the degree of determinism or nondeterminism of a particular mode of a predicate is known at compile time.

So, for example, a mode of a predicate which has one solution and cannot fail is deterministic, which is called det in Mercury. A mode which has one solution but can fail is called semideterministic; this, for example, is the type of a test predicate. The case of many solutions is also split into two: nondeterministic if it can fail, and multideterministic if it cannot.

The interesting cases for the purpose of this question is when a procedure can have zero solutions:

                Maximum number of solutions
Can fail?       0               1               > 1
no              erroneous       det             multi
yes             failure         semidet         nondet

Despite the names failure and erroneous, these are not incorrect determinism categories. The failure category is the determinism of fail, the goal that always fails. And erroneous is the determinism of a fatal error (called error/1 in Mercury) or raising an exception.

This is an important part of the semantics of the language. It means that you can write code like this:

sqrt(X) = SquareRoot :-
    ( if math_domain_checks, X < 0.0 then
        throw(domain_error("math.sqrt"))
    else
        SquareRoot = unchecked_sqrt(X)
    ).

If the call to throw/1 were not declared as erroneous, this would be a compile error. Both branches of the if-then-else must bind a value to the variable SquareRoot for it to be available to return from this function. But it is not an error here because the compiler knows that the then path is a dead-end, so it never has to supply a value.

$\endgroup$

You must log in to answer this question.

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