Hot++

try, catch and runtime errors

try, catch, finally

You throw and catch C++ exception types. A finally block always runs. A script can declare an exception type (see “Script exception types”). It can also catch the builtin types NullReference, IndexOutOfRange, ArithmeticError, NativeError, ScriptError, TaskCancelled and TaskReloadCancelled (see “Runtime errors”).

class Safe {
    int Div(int x, int y) {
        try {
            if (y == 0) throw std.runtime_error("divide by zero");
            return x / y;
        } catch (std.exception e) {
            std.printf("error: %s\n", e.what());
            return 0;
        } catch (...) {
            throw;
        } finally {
            std.printf("done\n");
        }
    }
}

Runtime errors

The runtime checks a pointer, a handle and an index of an array, a std::vector or any type with the stub attribute checked_index (see Native interop). A failed check throws one of these builtin types:

Write the type name in a catch clause. The prefix hot. is also valid. Use catch (ScriptError e) to catch all of them. e.what() gives the message.

class Grid {
    int[4] vals;
    Weapon weapon = null;

    int At(int i) {
        try {
            return this.vals[i];
        } catch (IndexOutOfRange e) {
            return -1;
        }
    }

    int Dmg() {
        try {
            return this.weapon.dmg;
        } catch (ScriptError e) {
            std.printf("error: %s\n", e.what());
            return 0;
        }
    }
}

A throw does not roll back. A field keeps each value that the script wrote before the throw. A script that does not catch the error passes it to the host. See Error reports. The host can remove the checks at build time (HOT_RUNTIME_CHECKS in CMake). Then a null access or a bad index is undefined behavior.

Script exception types

A struct with the base ScriptError is an exception type. The base can also be written hot.ScriptError, or it can be another exception struct. No new syntax is needed.

struct GameError : ScriptError {
    int code = 0;

    GameError(int c) : base("game error") { code = c; }
}

struct NetError : GameError {
    NetError(int c) : base(c) { }
}

class Player : Actor {
    int last = 0;

    void Run() {
        try {
            throw NetError(7);
        } catch (GameError e) {
            last = e.code;
            std.printf("%s\n", e.what());   // game error
        }
    }
}

An exception struct is for throw and catch only. It cannot be the type of a field, a module variable or an element of a container such as std.vector<E>. A reload moves the state of a field with the Nested migration, and this migration does not keep the message of the base. A local variable and a parameter can have the type.

Change of a container in a foreach

A change of the shape of a container while foreach (var x in c) walks it is undefined behavior in C++. The translator reports a compile error for such a change in the loop body. The shape changes are c = ..., and a call of push_back, insert, erase, clear, resize, emplace, pop_back and the other methods that can move or remove elements. Collect the changes in a new list, and apply them after the loop. A write to an element (c[i] = v, x.field = v) and a change of a different container are allowed. The check has limits. See Limits.

Checks against crashes

Each row shows how HotScript stops one cause of a crash. The mechanism is a compile error (the translator rejects the script), a runtime exception (a recoverable ScriptError), or unchecked. What can still crash lists the unchecked cases.

Cause Mechanism Exception Test
Null handle, pointer or optional Compile error where the analysis sees null. Else runtime. NullReference golden 55-59; NullDerefCaughtInScript; StarNullCaught; RuntimeChecks.DerefOfNullPointerThrows, DerefOfAnEmptyOptionalThrows
Null interface Runtime ScriptError CallOfANullInterfaceThrowsAScriptError; Fixture.NullHandleThrows
Null or expired CheckedPtr Runtime, always on NullReference CheckedPtr.ArrowOnNullThrows, ArrowOnExpiredThrows; CheckedPtrFieldReadsNullAfterTheTargetDies
Null handle to a native T& Runtime, always on NullReference NullHandleToNativeReferenceParameterIsCatchable; NullAlwaysOn.*
Index out of range: vector, array, span, string Runtime IndexOutOfRange IndexOutOfRangeCaughtInScript; ViewParameterConvertsAndChecksIndex; RuntimeChecks.*IndexOutOfRangeThrows
Missing key in a map Runtime, at only IndexOutOfRange or NativeError EmptyContainerOperationsAndNativeExceptionsAreCatchable; RuntimeChecks.IndexOfMapUsesItsOperator
Empty front, back, pop Runtime, always on IndexOutOfRange EmptyContainerOperationsAndNativeExceptionsAreCatchable; container_ops_tests.cpp
Dangling ref Compile error None golden 27, 28, 82-91, 94-103, 108
Dangling view Compile error None golden 105-107
Container change in foreach Compile error None golden 92, 93, 104
Use after reload A handle stays valid. A task of the old code is cancelled. TaskReloadCancelled AsyncHost.ReloadCancelsTheSuspendedTask; TaskReload.*; CheckedPtrFieldKeepsItsTargetAcrossReload
Use after a native free CheckedPtr and handles are checked. A raw T* is unchecked. NullReference CheckedPtrFieldReadsNullAfterTheTargetDies
Integer divide or modulo by zero Runtime, always on ArithmeticError IntegerDivideAndModuloAreChecked; arith_tests.cpp
Shift out of range Runtime, always on ArithmeticError ShiftsOverflowAndFloatCastsAreDefined; arith_tests.cpp
Signed overflow, float to int cast Defined result (-fwrapv, saturation) None SignedOverflowWrapsWithTheOptimizer; ShiftsOverflowAndFloatCastsAreDefined
Deep recursion Runtime, always on ScriptError("stack overflow") InfiniteRecursionIsCaughtInScript; RecursionLimitIsExactAndCanChange
Bad as or derived cast Gives a null handle. The use is checked as null. NullReference BadAsAndDerivedCastGiveNullAndTheUseThrows; IsAs.ScriptClassesWalkTheParentRecords; CheckedUnwrapCastThrowsOnNull
await on a dead task Runtime ScriptError (TaskCancelled for a cancel) TaskTests.EmptyTaskAwaitThrows, DestroyOfAnAwaitedChildGivesTheParentAnError; TaskReload.AwaitOfATaskCreatedBeforeAReloadGetsTheError