Question about try-after in Core Erlang

I was looking at how to represent try-after in Core Erlang, and noticed something odd. Given

z(E, H, A, X) -> try E() catch _C:_R -> H() after A(X) end.

erlc +to_core (OTP-25.0.3) generates

'z'/4 =
    %% Line 4
    ( fun (_0,_1,_2,_3) ->
          try
              try
                  apply _0
                      ()
              of <_4> ->
                  _4
              catch <_7,_6,_5> ->
                  apply _1
                      ()
          of <_9> ->
              do  apply _2
                      (_3)
                  _9
          catch <_12,_11,_10> -> % (1)
              do  apply _2
                      (_3)
                  primop 'raise'
                      (_10, _11) % (2)
      -| [{'function',{'z',4}}] )

which looks fine (a nested try, the outer one deals with the after logic).

However, if the inner try raises an exception, we end up at (1) and bind _12, _11 and _10 to the Class, Reason and (raw) Stacktrace of the exception, perform the after A(X) call, and then re-raise the exception at (2). But at (2) we only pass the (raw) Stacktrace and Reason, not the Class. Yet when running this the correct exception class (exit, error, or throw) is propagated from A(X) to the caller of z/4.

Is there some side-channel which propagates the exception class even though the intermediate code (Core Erlang) doesn’t do so?

4 Likes

I’m not 100% sure, but based on the following paragraph from Core Erlang 1.0.3 specification:

An exception is a value Îľ describing an abrupt termination of evaluation. In
implementations of Erlang, this is a pair (r, x), where r is an arbitrary Erlang
value, usually referred to as the “reason” or “error term” of the exception, and
x is an implementation-dependent Erlang value that specifies further details
about the exception.
For any such x, at least one primitive operation must be defined:
exc class(x)
which yields an atom representing the class of the exception - in Erlang, this
is currently one of ’error’, ’throw’, or ’exit’.

I think that there exists an internal function that can resolve the exception class in the runtime based on the stacktrace.

I hope we’ll get a more detailed answer soon :smiley:

3 Likes

Yes. The class is stored in the stack trace term.

Correct. It is the following function in beam_common.c:

void erts_sanitize_freason(Process* c_p, Eterm exc) {
    struct StackTrace *s = get_trace_from_exc(exc);

    if (s == NULL) {
        c_p->freason = EXC_ERROR;
    } else {
        c_p->freason = PRIMARY_EXCEPTION(s->freason);
    }
}

It is used like this in the threaded (non-JIT) interpreter:

i_raise() {
    Eterm raise_trace = x(2);
    Eterm raise_value = x(1);

    c_p->fvalue = raise_value;
    c_p->ftrace = raise_trace;
    erts_sanitize_freason(c_p, raise_trace);
    goto find_func_info;
    //| -no_next
}
4 Likes