For example in rust most code that can fail will return a Result and thus the compiler forces you to handle that. However, that code can just as easily panic and behave like an uncaught exception would (thread exiting). An example would be the division operator and the array index operator. Both division-by-zero and out-of-bounds errors can certainly be handled by using a Result but in this case the Rust developers made a decision to use panic. Are these both exceptions bc they are handled like a typical uncaught exception or are they errors bc it’s conceivable to handle them just like a failed file open?
Off-topic, but why does nobody use those? The idea of "I have an exception, I'm the best place to act on it but the worst place to decide what action to take, request for advice" sounds good, at least
Does your definition of a “fault” mandate that the process be killed? I’m sure there are architectures where forcing this to occur would require checks.
There might exist ISAs without any equivalent to faults—but I would guess that the semantics of any "faultable"-instruction-equivalent on such an ISA, would 1. actually take the slow path and use a flag register to return the status of the execution; and 2. would thus require C and C-like languages to generate shim code that e.g. checks for that flag result on every use of the dereference operator, in order to generate a userland-simulated "fault"-equivalent (i.e. an abort(3)). This is because these languages don't have any way to capture or represent the potential flag-result return value of these operations. Low-level languages expect dereferencing to raise exceptions, not return errors. There's no place for the errors to flow into.
> Low-level languages expect dereferencing to raise exceptions, not return errors.
Are you talking about C? Because it doesn’t actually define anything in this case. Sure, on most OS/architecture combinations, it will trigger a memory protection violation, but this is not guaranteed and I’m sure that in these cases the compiler doesn’t literally insert checks before dereferencing anything to guarantee failure.
Yeah, the "C abstract machine" that most C-like languages rely on by the fact of relying on libraries like libc.
> Because it doesn’t actually define anything in this case.
To be clear, when I said expect, I meant expect. These languages expect invalid dereferences to fault—i.e. to cause control flow to be taken out of the program's hands. Which is to say, they expect a generated dereferencing instruction that returns to have put the system into a valid state.
But in practice, what that means is that compilers just expect dereferences to always be valid (because any dereference that returns is valid.) So they don't have to generate any check for null before doing a dereference; and they don't have to worry about having any way to represent the result of a null dereference.
Another way to say that is that dereferencing null pointers is implicit undefined behavior. There's no part of the spec that covers what would happen if you dereferenced a null pointer, because in the model as the spec lays it out, dereferencing is a happy, clean little operation that never goes wrong. (i.e. "The MMU might cause a CPU fault? Who cares! My program didn't cause that fault; it was the MMU's choice to decline to read from $0. Other MMUs could totally allow that, and then LEA would be defined for the entire domain.") Same goes for reading from unmapped regions—"it's the MMU's fault; it could have just as well returned 0, after all."
> I’m sure that in these cases the compiler doesn’t literally insert checks
Yep, you're right. Again, it's because this is what these languages expect. It's not quite the same as undefined behavior; it's that the "C abstract-machine evaluation model" was defined to assume that architectures will always have their IDIV-equivalent instruction fault if invalid. If it returns a flagged value on some arch instead, that's something the C evaluation model is not prepared to deal with.
(Technically, I believe that you'd say that a C compiler "cannot be written" for such an arch while retaining conformance to any existing C standard, and for such an arch to gain a conformant C compiler, a new C standard draft would have to be written that specifies an evaluation model for such architectures—as then the compiler could at least be conformant to that.)
Which is helpful to know: just compiling C for such an arch at all, is undefined behavior, no matter what you write ;)
Dividing by zero is an exceptional situation (at the level of the processor, even!), rather than an error, because most divisions have known, nonzero divisors; certainly, all intentional divisions do. If you do end up telling the processor to divide by zero, it is assumed that you fucked up and didn't write your program right, because user-supplied divisors are a very rare use-case compared to known-domain divisiors, and a known-domain divisor being zero is indeed programmer error.
But, even if cases where the user controls the divisor are comparatively rare, they do exist. So, even if the processor isn't going to help you, why not have the compiler generate a check for them, such that integer division would be an (Either Integer Error) kind of operation? Well—performance.
Integer division—in languages where it generates an exception—is a low-level primitive. It's meant to be fast. (In fact, all integer operations in such languages are meant to be fast single-CPU-instruction-equivalent primitives; this is also why e.g. overflow isn't checked on integer addition.) Compilers for low-level languages are expected by their users to not generate any protective code for these primitives, because that protective code would get in the way of using the primitives at the highest efficiency possible. 99% of the time, the domains of these operations are fixed at the business level, such that these errors cannot occur. And, the other 1% of the time [like when writing a calculator program], the users of these languages use "safe math" libraries built on top of these primitive operations, rather than the primitive operations themselves.
Let me put this another way, with a concrete example.
In Rust, in safe code, you can't dereference arbitrary pointers, only opaque abstracted pointers (references) given to you by the runtime, that are guaranteed by the compiler to have non-null [direct] contents. You can make the inner type of a RefCell an Option<Foo> instead of just a Foo, and then it can be null... but this kind of null is an instantiation of the Maybe monadic sum-type, and so the compiler can notice when you aren't checking for its existence and stop you.
But in Rust, in unsafe code, you can dereference an arbitrary pointer, and the result can be null. What should happen when you do so? Well, that's an exceptional situation. You asked for unsafety, and then you did the stupid thing you weren't supposed to do. There's nothing that can really save your code now.
Before the invention of "exceptions", we just called these types of errors faults. Protection faults, for example. Your code would just be killed, without the ability to do anything, because it just did something that broke the semantics of the abstract machine (the process) that the OS had the program boxed up in. The OS might be kind enough to core-dump your process's memory in the process of killing it.
Exceptions are still faults. They just let you do arbitrary other stuff as your process comes tumbling down. There's nothing you, or the runtime, can do to "save" your process, when the "error" in question is precisely that you first asked your compiler for unsafety—asked it to not prevent your code from compiling if it does something invalid—and then you went ahead and used that unsafety to do something invalid.
In Erlang land, we have a third thing: exits. Exits are faults on a small scale—they kill the actor-process that generates them, and then spread across its linked siblings and parents to kill those too, until/unless one of them is "trapping exits", at which point that actor-process will receive the exit as an informational message from the runtime rather than just dying. Most actor-processes don't trap exits; and, in fact, you can't trap an exit generated by your own actor-process, only an exit "echoed" to your actor-process from below/around you. And the only processes that do trap exits, don't attempt to "save" the actor-process that is dying. Unlike with exception-unwinding, by the time an exit is "heard" by another actor-process, the actor-process that emitted the exit is already dead. The point of trapping exits is to decide what to do after the actor-process dies—for example, starting another one of the same actor-process to replace it.
(As it happens, POSIX exit statuses fit this concept as well, though semantics like bash scripts not being `set -e` by default kind of screws that up.)
Exceptions have their place—they describe a fault-in-progress, and let arbitrary things happen as a fault causes a stack to unwind, before the process dies. Exits also have their place—they let processes react to other processes having faulted. And errors have their place—they're a branch of the Either monad that a compiler can force your code to handle.
Personally, I don't see what confusion there is between any of these. They're roughly orthogonal.
And none of them represents "something bad happened from this function call. Somebody up there on the stack, help me recover, please!" (Those would be Lisp conditions, but nobody uses those.)