Control Flow
if, while, do, both for loops, switch and try/catch/throw read as they do in C++. Four things differ:
- A
switchcase 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 anerror. - A mistake the checker can see fails the load, so no
catchever sees it. - A script that outruns its execution budget ends with a terminal error that no
catchsees.
if, while, for, do
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);odd
12 5 4 1breakoutside a loop orswitch, andcontinueoutside a loop, are parse errors ('break' outside a loop or switch). A function or lambda body leaves nothing it sits in.- A
docondition cannot see the body's locals.dois a keyword only before a{; elsewhere it is an ordinary name. - There is no
goto; writing one is an error that points tobreak,continueandreturn. - A plain
=as the whole condition of anif,while,doorforwarns: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 anincludereads). - A C-style
forvariable 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).
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);- sword
- shield
alice scored 3
bob scored 5
1
2
["sword!", "shield!"]| Loop variable | What it is |
|---|---|
auto x | a 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& x | the element itself: writes change the container (Value Semantics). |
T x | T 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& x | binds 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++.
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"));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. defaultruns 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, soswitch (1)matchescase 1.0and an instance matches memberwise or through itsoperator==(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
returnworks inside a case. In a switch inside a loop,breakleaves the switch only andcontinuereaches 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:
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; }));missing file
load error: load
code 42
out of range
error: Division by zero
something else: a float| Clause | Takes |
|---|---|
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), catch | everything; it comes last (a catch with no type catches everything: it is the last handler of its try) |
enames the thrown value itself; no copy is made, where C++ copies.&changes nothing, andconstmakes it read-only.- With no clause for it, an error goes on to the next
tryout, from the place that raised it. - A bare
throw;in a handler re-throws the same value; with no active exception it raises the catchableBare throw with no active exception thrown. throwis 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.
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"); }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| Member | A run-time error | A thrown string, array, map or instance |
|---|---|---|
e.message | the error text | its display text (an instance's to_string()) |
e.value | null | e itself |
e.file, e.line, e.column | the statement that raised it | the throw |
e.stack | the script call stack as file:line strings, innermost first | the same |
- A thrown number, bool or null carries no place: it answers
e.valueande.messageonly. 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
tryabove 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, abreakor a new throw), as a C++ exception object does. A barethrow;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):
try {
int n = "seven";
} catch (e) {
print("never printed");
}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.
try {
while (true) { }
} catch (e) {
print("never printed");
}== error: Script execution budget exceeded - raise engine::execution_budget or break up the workThe 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.