LEVIATHAN v0.1 · in development

Control & Errors

Statements & Control Flow

Every statement form the parser accepts: declarations, blocks, branches, and the four loop shapes. Conditions require bool — there is no truthiness anywhere in the grammar.

Type name = expr;      var name = expr;       // declaration (var/let infer)
Type name;                                     // auto-constructed
using Type name = expr;                        // deterministic cleanup: see Errors & Resources
expr;                                          // expression statement
{ stmt* }                                      // block
return expr;    => expr;                       // return (arrow form)
if (cond) stmt [else stmt]
while (cond) stmt
do stmt while (cond);                          // post-test: body runs before the first check
for (init; cond; step) stmt
for (T x in iterable) stmt                     // ranges, arrays, maps (Pair entries), any IIterable<T>
break;      continue;                          // loop control
throw expr;                                    // see Errors & Resources
try stmt catch (Type name?) stmt [catch ...]   // see Errors & Resources
bind ...;   uses NS;   use NS::name (as alias)?;   // imports: see Namespaces & Injection
;                                              // empty

Declarations & assignment

A declaration names a type and binds it: Type name = expr; for an explicit type, or var/let to infer the type from the initializer. var and let are pure inference markers, not types — they differ only in mutability. let is sugar for const var: a single-assignment inferred binding. var stays freely reassignable.

int count = 0;          // explicit type
var name  = "Ada";       // inferred, reassignable
let id    = nextId();    // inferred, single-assignment (const var)

A bare declaration with no initializer — Type name;auto-constructs to that type's default: string s; is "", Array<T> a; is [], int i; is 0. Auto-construction of arbitrary object types is currently partial — primitives and arrays are covered.

string s;   // ""
int i;      // 0
Array<int> a;  // []

const composes with either form (const int max = 50;, const var limit = 100;) and fixes the slot after its initialization window closes. See Types & Generics for the full const/readonly story.

Blocks & expression statements

Any expression followed by ; is a statement — most often a call or an assignment. A block { stmt* } groups statements and introduces its own scope; it is itself a valid statement anywhere one is expected, including as the body of an if, loop, or function.

log.info("starting");     // expression statement (a call)
count = count + 1;        // expression statement (an assignment)
{
    var tmp = compute();
    use(tmp);
}                          // block: tmp is out of scope after this

A return exits the enclosing function, either as a full statement or the arrow shorthand used in expression-bodied members:

int square(int x) { return x * x; }
int square2(int x) => x * x;

if / else

if (cond) stmt [else stmt]. cond must be bool — Leviathan has no truthiness: an int, a string, or an optional value cannot stand in for a condition. Chained conditions are just an else whose statement is another if.

if (n < 0) {
    sign = -1;
} else if (n == 0) {
    sign = 0;
} else {
    sign = 1;
}
Conditions require bool

Every construct that branches on a condition — if, while, the do-while test, and the C-style for middle clause — takes exactly a bool expression. There is no implicit conversion from any other type, so if (x) is only legal when x is already bool. An if/while condition also narrows a union tested with != None/== None/is in its branches.

while

while (cond) stmt — a pre-test loop: cond is checked before every iteration, including the first, so the body may run zero times.

int i = 0;
while (i < 10) {
    print(i);
    i = i + 1;
}

do-while

do stmt while (cond); — a post-test loop: the body runs once unconditionally, then the loop continues while cond holds. Unlike while, the body always executes at least once.

int attempts = 0;
do {
    attempts = attempts + 1;
} while (attempts < 3 && !succeeded());

continue inside a do-while body jumps to the condition check, not back to the top of the body — the same target while uses, just reached from mid-body.

C-style for

for (init; cond; step) stmt — the classic three-clause loop. init runs once before the loop; cond (must be bool) is checked before every iteration; step runs after each iteration, before the next cond check.

for (int i = 0; i < 10; i = i + 1) {
    print(i);
}

continue in a C-style for jumps to step (then re-checks cond) rather than skipping straight to the top of the body.

for-in

for (T x in iterable) stmt iterates a Range, an Array, a Map (yielding Pair entries), or any type implementing IIterable<T>. for (var x in e) infers the loop variable's type from what e yields.

for (int i in 1..5) { print(i); }              // range

for (string s in names) { print(s); }           // array

for (Pair e in scores) {                        // map: entries as Pair<K,V>
    print("${e.first} = ${e.second}");
}

The two prelude interfaces that make a user type for-in-able:

interface IIterator<T> { bool hasNext(); T next(); }
interface IIterable<T> { IIterator<T> iterator(); }

When e's static type implements IIterable<T>, for (T x in e) stmt desugars to:

var __it = e.iterator();
while (__it.hasNext()) { T x = __it.next(); stmt }

break/continue behave exactly as in that hand-written while — a continue re-checks hasNext(). A type implementing neither a built-in collection nor IIterable<T> is a compile error naming the protocol. The checker actually picks a loop's dispatch path statically — a range literal is a counted loop with no object allocated, and Array/Map/Range take a fast IterLen/IterAt path rather than routing through the protocol — see Collections & Iteration for the full dispatch-order contract.

match

match (subject) { pattern => body; ... } dispatches on a type pattern (Type =>, narrows the subject in that arm), a value or range pattern (0 =>, 1..9 =>), or else. Arms are tried first-match-wins; a closed union is exhaustive without else, while an open class hierarchy requires one.

match (m) {
    Method::GET  => handleGet();
    Method::HEAD => handleHead();
    Method::POST => handlePost();
}   // exhaustive: closed union, no else needed

match (status) {
    Status::OK => "ok";
    else       => "other";
}

match is a statement when its arms end without a trailing ; after the block form, and an expression when its arms yield a value — the same match syntax works in either position. Bodies are either => expr; or => { block }. Under the hood match lowers to the same is/type-dispatch test used by catch and is — one dispatch path for the whole language. Full arm-matching semantics and match as a value-producing expression are covered in Expressions & Operators; this page only covers its shape as a control-flow statement.

match is not a loop boundary

match does not create a loop-nesting scope. A break/continue inside a match arm that itself sits inside a loop targets that enclosing loop, exactly as if the match weren't there.

break & continue

break; exits the nearest enclosing loop (while, do-while, C-style for, for..in). continue; skips to the next iteration — for while/do-while, to the condition; for C-style for, to the step (then the condition); for for..in, to the next element.

for (int i = 0; i < 100; i = i + 1) {
    if (i % 2 == 0) { continue; }   // next i
    if (i > 50) { break; }          // stop the loop
    print(i);
}

Both are unlabeled only — they always target the innermost enclosing loop, and using either outside of any loop is a compile error.

A lambda body is its own loop-nesting scope

A bare break/continue inside a lambda never reaches a loop in the enclosing function — it is legal only when the lambda's own body has a loop of its own.

for (int i in 0..9) {
    items.forEach((x) => {
        // break; here would be a compile error —
        // this lambda's body has no loop of its own.
    });
}

match arms do not count as loop nesting either — see the callout above.

Exceptions & using (preview)

Two more statement forms exist for control transfer and cleanup, but get full treatment on their own page:

throw expr;                                    // expr must implement IException
try stmt catch (Type name?) stmt [catch ...]   // first assignable clause wins
using Type name = expr;                        // deterministic cleanup on every exit path

throw is a control-transfer statement like return; try/catch selects the first clause whose declared type the thrown value is assignable to; using declares a scope-owned IDisposable resource whose close() runs on every way the declaring block can exit. See Errors & Resources for the exception hierarchy, catch-clause selection, and the full using cleanup contract.