Reading a Recovered Error — Design

Issue: #1247 (typed catch, finally, rethrow, and reading the error in a ?-> handler), split out of #1186. This covers the issue's smallest useful step: letting the code after a ?-> read what went wrong.

Why

?-> (->Try()->) turns any runtime error raised below a guarded call into Nil (or the ?? default), and execution continues. Every recovered error looks the same: the runtime prints the message only when nothing is guarding. Code that must react differently to a missing key and a divide by zero had to record a note in shared state before raising, and the test rosetta_exceptions.obs documents that workaround. Research code recovers from errors constantly (a bad sample, a failed parse, a timeout), and it needs to log which error and why.

value := table->Get(key)?->Parse() ?? 0;
if(Runtime->HasError()) {
  kind := Runtime->GetErrorKind();      # Runtime->ErrorKind->INVALID_CAST, ...
  "skipped {$key}: {$(Runtime->GetError())}"->ErrorLine();
  Runtime->ClearError();
};

Decisions

| Runtime->ErrorKind | raised by | |---|---| | NONE | no recovered error, or cleared | | NIL_DEREFERENCE | a method call or field access on Nil | | INDEX_OUT_OF_BOUNDS | an array index outside the array | | DIVIDE_BY_ZERO | integer or float division or modulo by zero | | INVALID_CAST | ->As(T) on an object that is not a T | | OTHER | any other runtime error (a trap or native call) | | RAISED | Runtime->Raise (ERROR_HANDLING_DESIGN.md) |

API (System.Runtime, appended traps)

function : HasError() ~ Bool         # a recovered error is recorded
function : GetError() ~ String        # its message, or Nil
function : GetErrorKind() ~ Runtime->ErrorKind
function : ClearError() ~ Nil

The four new traps are appended after the last existing trap (the memory rule: a trap's number and arity are never changed).

Not in this step

These are the rest of #1247 and stay open:

They need syntax and their own design. This step makes them easier to judge, because code can now see the errors it recovers from.

Implementation

  1. VM: a RecoveredError (kind and message) next to the per-thread TryHandlerState.
    • TryErrorRecovery(stack_pos, kind, message) records it when it recovers. Its call sites already raise one error each, so each passes its own kind and text.
    • The bridge's status path maps the JIT statuses (−1 to −5) to kinds when it recovers for a compiled frame.
  2. Traps: RUNTIME_HAS_ERROR, RUNTIME_GET_ERROR, RUNTIME_GET_ERROR_KIND and RUNTIME_CLEAR_ERROR, added to traps.h. Their handlers go in the trap processor, and linker.cpp needs nothing because traps are operands, not opcodes.
  3. lang.obs: the four functions and the ErrorKind enum on Runtime. lang.obl is rebuilt.

Tests

recovered_errors.obs covers each kind:

rosetta_exceptions.obs keeps its shared-note workaround for now. Moving it to Runtime->GetErrorKind() belongs with typed catch, which would replace both.