Control Flow​

if, while, do, both for loops, switch and try/catch/throw read as they do in C++. Four things differ:

  • A switch case ends where the next one begins; fallthrough; continues on purpose.
  • Any value can be thrown, catch (Type e) selects by type, and the engine's own run-time errors arrive as an error.
  • A mistake the checker can see fails the load, so no catch ever sees it.
  • A script that outruns its execution budget ends with a terminal error that no catch sees.

if, while, for, do​

jaiscript
auto n = 7;
if (n % 2 == 0) { print("even"); } else { print("odd"); }
auto total = 0;
for (int j = 0; j < 10; ++j) {
    if (j == 3) { continue; }
    if (j == 6) { break; }
    total += j;
}
auto i = 1;
while (i <= 4) { i++; }
int tries = 0;
do {
    tries++;
    if (tries == 2) { continue; }      // continue goes to the condition
} while (tries < 4);
int once = 0;
do { once++; } while (false);          // the body runs before the first test
print(total, " ", i, " ", tries, " ", once);
output
odd
12 5 4 1
  • break outside a loop or switch, and continue outside a loop, are parse errors ('break' outside a loop or switch). A function or lambda body leaves nothing it sits in.
  • A do condition cannot see the body's locals. do is a keyword only before a {; elsewhere it is an ordinary name.
  • There is no goto; writing one is an error that points to break, continue and return.
  • A plain = as the whole condition of an if, while, do or for warns: assignment used as a condition: write '==' to compare, or '((a = b))' with parentheses of its own to assign. if ((a = next())) is silent.
  • A { that starts a statement opens a block, so a map literal written there goes in parentheses. At a file's top level {"key": value} is the file's value (a data file an include reads).
  • A C-style for variable that a closure captures by reference is each iteration's own (Lambdas and captures).

Range-based for​

for (auto x : c) walks an array's elements, a string's chars, a map's entries (kv.first and kv.second, or for (auto [k, v] : m); Maps) and a coroutine's yields (Coroutines).

jaiscript
auto items = ["sword", "shield"];
for (auto item : items) { print("- ", item); }       // item is a copy
for (auto& s : items) { s = s + "!"; }               // auto& is the element itself
auto scores = {"alice": 3, "bob": 5};
for (auto [name, score] : scores) { print(name, " scored ", score); }
var loose = [true, 2];
for (int v : loose) { print(v); }                    // int v = element; per element
print(items);
output
- sword
- shield
alice scored 3
bob scored 5
1
2
["sword!", "shield!"]
Loop variableWhat it is
auto xa copy of each element, with the elements' static type (int over an array<int>, var over a var array). A copy of a shared_ptr element is another handle to the same object.
auto& xthe element itself: writes change the container (Value Semantics).
T xT x = element; per element, with that declaration's conversions and errors. Any type may be named: shared_ptr<Node> n, const array<int>& row.
T& xbinds only an element that holds a T (a class: the class or a subclass). A mismatch fails the load when the checker knows the element type, and a var container is judged element by element.

A write into a by-value loop variable that nothing reads afterwards fails the load: 'x' writes to 'x', a copy of the element that is never read afterwards, so the write is lost; iterate with 'auto& x' to change the element (Lost writes).

switch: break by default, explicit fallthrough;​

Unlike C++, a case ends where the next one begins; fallthrough; continues into the next case on purpose. Stacked labels share a body, as in C++.

jaiscript
function describe(weapon) {
    auto damage = 0;
    switch (weapon) {
        case "sword": case "blade":        // two values, one body
            damage = 10;
        case "bow": damage = 8;
        case "magic":
            damage = 15;
            fallthrough;                   // continue into the next case
        case "enchanted": {
            int bonus = 5;                 // a declaration needs the body in braces
            damage += bonus;
        }
        default: damage = 1;
    }
    return damage;
}
print(describe("sword"), " ", describe("blade"), " ", describe("magic"), " ", describe("enchanted"), " ", describe("fists"));
output
10 10 20 5 1
  • A case with statements never falls through without fallthrough;. A case with no statements shares the body of the label below it; an empty last label does nothing. fallthrough; outside a switch is an error.
  • default runs when no case matches, wherever it is written. It shares a body only as the last label: a label that shares a body with 'default' needs 'default' as the last label of the switch.
  • A case value is any expression: an integer, a string, an enum member, a call. It matches when the language's == holds, so switch (1) matches case 1.0 and an instance matches memberwise or through its operator== (an error that operator raises leaves the switch).
  • A declaration in a case body without braces is a parse error: a declaration in a case body needs the body in braces: case value: { ... }.
  • Switches nest, and return works inside a case. In a switch inside a loop, break leaves the switch only and continue reaches the loop.

Exceptions: try, catch, throw​

Any value can be thrown. A try has any number of catch (Type e) clauses, tried in order, and the first that takes the thrown value runs:

jaiscript
class LoadError { string what = "load"; }
class MissingFile : LoadError { }
string handle(var action) {
    try { action(); }
    catch (MissingFile e) { return "missing file"; }
    catch (LoadError e) { return "load error: " + e.what; }
    catch (int code) { return "code " + to_string(code + 1); }
    catch (error e) { return e.kind == error_kind::bounds ? "out of range" : "error: " + e; }
    catch (e) { return "something else: a " + type_of(e); }
    return "ok";
}
print(handle(() => { throw MissingFile(); }));
print(handle(() => { throw LoadError(); }));
print(handle(() => { throw 41; }));
print(handle(() => { auto xs = [1]; xs[4]; }));
print(handle(() => { int zero = 0; print(1 / zero); }));
print(handle(() => { throw 2.5; }));
output
missing file
load error: load
code 42
out of range
error: Division by zero
something else: a float
ClauseTakes
catch (Class e)an instance of the class or a subclass
catch (int e), catch (string e), catch (array<int> e), ...that builtin kind and no other: an int is not a float
catch (error e)a run-time error the engine raised (a bad index, a division by zero, a failed cast, a type mismatch); nothing a script throws
catch (e), catch (var e), catch (auto e), catcheverything; it comes last (a catch with no type catches everything: it is the last handler of its try)
  • e names the thrown value itself; no copy is made, where C++ copies. & changes nothing, and const makes it read-only.
  • With no clause for it, an error goes on to the next try out, from the place that raised it.
  • A bare throw; in a handler re-throws the same value; with no active exception it raises the catchable Bare throw with no active exception thrown.
  • throw is an expression: x > 0 ? x : throw "negative".
  • There is no finally: a destructor runs when its scope ends, a throw included.
  • An uncaught throw reaches the C++ host as an exception with the script stack trace attached. A bound C++ function, method, operator or accessor that throws surfaces as a catchable script error with the exception's text.

What the caught value carries​

A run-time error is an error: type_of(e) is "error", e.kind is its family (error_kind::bounds; the reference lists them), and e behaves as its message string everywhere: printed, compared (e == "text"), joined (e + "..."), stored in a string, or under the string methods. The same text is the host's what() when the error escapes.

jaiscript
int pick(array<int> xs, int i) {
    return xs[i];
}
try { pick([1, 2, 3], 7); } catch (e) {
    print(e, " (", e.kind, ", line ", e.line, ", value ", e.value, ", stack ", e.stack.size(), ")");
}
class Oops { int code = 7; }
try { throw Oops(); } catch (e) { print("code ", e.code, " at line ", e.line); }
try { try { throw 42; } catch (e) { print(type_of(e), " ", e + 1); throw; } } catch { print("rethrown"); }
output
Array index 7 out of bounds for array of size 3 (bounds, line 2, value null, stack 2)
code 7 at line 8
int 43
rethrown
MemberA run-time errorA thrown string, array, map or instance
e.messagethe error textits display text (an instance's to_string())
e.valuenulle itself
e.file, e.line, e.columnthe statement that raised itthe throw
e.stackthe script call stack as file:line strings, innermost firstthe same
  • A thrown number, bool or null carries no place: it answers e.value and e.message only. An instance's own member of one of these names is read first.
  • A stack deeper than 64 frames lists the innermost 64, then one line ... N more frames. On the VM a bare self tail call (return f(n - 1);) reuses its frame, so a tail recursion shows one entry where the interpreter lists every level.
  • Counting the frames beneath the 64 takes time in proportion to the depth: a loop that throws and catches at the bottom of a deep recursion with no try above it counts every frame on every throw, while a catch and rethrow at every level stays linear.
  • The thrown value lives until the handler that caught it exits (by completing, a return, a break or a new throw), as a C++ exception object does. A bare throw; carries it on.
  • A copy of an error or of a thrown string kept past its handler still answers its message, place and stack, with a null e.value; a new string built from one (e + "") is plain text. A thrown array, map or instance answers its place only while its handler runs.

What never reaches a catch​

The checker reads the whole program before anything runs. A mismatch it can see (a literal, a typed variable, a typed return) fails the load, and a try around it changes nothing. A mismatch it cannot see, because the value came through a var, raises where it lands (Cannot initialize 'n' (declared int) with string) and is catchable there (The Type Ladder, Types That Fail Early):

jaiscript
try {
    int n = "seven";
} catch (e) {
    print("never printed");
}
output
jaiscript: static check failed:
<script>:2:9: error: cannot initialize 'n' declared 'int' with 'string'
        int n = "seven";
            ^

Terminal errors: the execution budget and the memory cap​

Every execute() and every coroutine next() has a wall-clock budget, 5.0 seconds by default (engine::execution_budget(seconds); 0 disables it). An overrun is terminal: every catch, typed or not, is skipped all the way out to the host, so a runaway script cannot swallow its own stop.

jaiscript
try {
    while (true) { }
} catch (e) {
    print("never printed");
}
output
== error: Script execution budget exceeded - raise engine::execution_budget or break up the work

The standalone runner has no budget unless given one: jaiscript --budget=1 runaway.jai.

engine::memory_cap(bytes) bounds allocation the same way. Its first raise in an execute is catchable (the growth is denied), and a second is terminal. Both limits re-arm per execute(), and neither can be swallowed inside a function, lambda or coroutine.