Question about how VM deals with exceptions

As far as I understand, the BEAM keeps last exception information saved into the process structure:

struct process {
    [...]
    Uint freason;               /* Reason for detected failure. */
    Eterm fvalue;               /* Exit & Throw value (failure reason) */
    [...]
    Eterm ftrace;               /* Latest exception stack trace dump */
    [...]
}

As I understand, there are some specific opcodes that allow to copy these internal fields to regular registers (x[0], x[1], x[2]).
Those opcodes are try_case, catch_end and stacktrace_build (this later write only to x[0]).

  • Did I understand correctly how the BEAM works?
  • Did I forgot any other opcode that writes exception information to regular x registers?

fvalue and ftrace are terms, so GC must treat them as live roots, therefore they may use some space in the process heap.
I assume they are not kept forever and at a certain point they are cleared up.

  • Which events or opcodes trigger a cleanup?
  • Are they cleaned up all at the same time?

So my assumpion is that after one of those cleanups raw_raise cannot work.
So my question can be also rephrased to: after which opcode it is not possible anymore to have raw_raise?

6 Likes

The fvalue and ftrace values are still roots in the GC, but they now live for a much shorter time than they used to. In fact, they are killed already in handle_error()in beam_common.c. The raw_raise instruction takes its input from BEAM registers, not from the process struct.

Here are two pull requests that may make this a little bit clearer:

5 Likes

I think we can consider them redundant these days, along with fcalls, since they are only relevant during execution and a very brief time at that. I believe it wouldn’t be difficult to move them to per-scheduler data and save ~4 words per process.

7 Likes

I looked at the code but I’m not sure when handle_error() is called, but it looks like my previous understanding is quite wrong.

By looking at this:

try_case(Y) {
$try_end($Y);
ASSERT(is_non_value(x(0)));
ASSERT(c_p->fvalue == NIL);
ASSERT(c_p->ftrace == NIL);
x(0) = x(3);
}

it looks like fvalue and ftrace are cleared up before executing try_case opcode. So indeed they look very short lived.

So why the BEAM is keeping those values as roots in the GC? Is handle_error() called from any specific opcode (I didn’t figure it yet) or is it immediately executed after returning from a BIF/NIF or during raise opcode ?

So now I’m also trying to understand when fvalue and ftrace are written to x[1], x[2], x[3] (and still trying to understand when they are cleared up).

The latter; the GC may in some cases be invoked immediately after returning from C but before actually raising the exception.

If we remove this invisible quirk, we can remove those fields from the process structure.

3 Likes

So, in other words, from an opcode point of view, once exception info is written to registers, it is not possible to get it again if registers are overwritten, since everything is cleared immediately before returning to opcodes excecution.

Yes.

(ā€œpost must be longer than 6 charactersā€)