MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.11

8. Control Structures

Control structures allow you to control the flow of execution in your program with conditionals and loops.

8.1 Conditional Statements

If Statement

if (condition) {
    // code block
}

If-Else Statement

if (condition) {
    // code block
} else {
    // code block
}

If-Else If-Else Statement

if (condition1) {
    // code block
} else if (condition2) {
    // code block
} else {
    // code block
}

8.2 Loops

While Loop

while (condition) {
    // code block
}

For Loop

The traditional for loop with initialization, condition, and increment:

for (var i = 0; i < 10; i = i + 1) {
    // code block
}

For-In Loop

The for-in loop iterates over each element in an array:

for (var component in components) {
    // code block - component is the current element
}

Example:

var numbers = [1, 2, 3, 4, 5];
for (var num in numbers) {
    print(num);  // Prints: 1, 2, 3, 4, 5
}

var names = ["Alice", "Bob", "Charlie"];
for (var name in names) {
    print("Hello, " + name);
}

Notes:

Foreach Loop

The foreach loop is equivalent to the for-in loop: it iterates over each element in an array. Use whichever form you prefer.

foreach (var item in collection) {
    // code block - item is the current element
}

Example:

var results = [10, 20, 30];
foreach (var x in results) {
    print(x);  // Prints: 10, 20, 30
}

The same rules as for-in apply: the loop variable is declared with var, the collection must be an array, and break and continue work as in other loops.

Break and Continue

while (true) {
    if (condition) {
        break;  // Exit loop
    }
    if (skipCondition) {
        continue;  // Skip to next iteration
    }
}

8.3 Pattern Matching

Pattern matching allows you to match values against patterns and execute code based on the match. It's similar to switch statements in other languages but more powerful, supporting pattern matching on arrays, objects, and nested structures.

Match Expression

The match expression evaluates a value against a series of patterns and executes the code for the first matching pattern:

var result = match value {
    case pattern1: expression1;
    case pattern2: expression2;
    default: defaultExpression;
};

Example:

var x = 42;
var result = match x {
    case 42: "matched 42";
    case 10: "matched 10";
    default: "no match";
};
print(result);

Match expressions can also be used as statements (without assigning to a variable):

match x {
    case 42: print("matched 42");
    default: print("no match");
}

Block Bodies and Multiple Statements

A case body can be a single expression or a block { ... } with multiple statements. This allows side effects (e.g. print, assignments) before producing the case value.

Last expression wins (match case bodies): When a match is used as an expression, a case block's value is the last statement's value if that statement is an expression. If the last statement is not an expression (e.g. print(...)), the match yields null. The same last expression wins rule also applies to functions, methods, and lambdas (see 9. Functions). Blocks in if, while, and for do not produce a value; they are purely imperative.

var result = match x {
    case 42: {
        print("side effect");
        "result";   // last expression is the case value
    }
    default: "no match";
};
print(result);  // Prints: "side effect" then "result"
var result = match x {
    case 42: {
        print("only side effect");
    }
    default: "no match";
};
print(result);  // result is null (block ends with statement, not expression)

Summary:

Pattern Types

Variant Patterns

For values built from sum types, use a variant pattern: ConstructorName(binding, ...). The value must be that variant and have the same number of payload slots; payloads are bound to the names in the pattern. A bare constructor name (case Ok: / case None:) is the same match with implicit _ for each payload — equivalent to case Ok(): for a unit variant. It does not bind a catch-all variable of that name.

type Result = Ok(value) | Err(message);
var r = divide(10, 0);
match r {
    case Ok(v): print("value: " + v);
    case Err(msg): print("error: " + msg);
}
type Result = Ok() | Err(message);
var r = Err("ciao");
var m3 = match r {
    case Ok: "ok: ";
    case Err(msg): "error: " + msg;
};
print(m3);

Literal Patterns

Match exact values (numbers, strings, booleans, null):

match x {
    case 42: "number";
    case "hello": "string";
    case true: "boolean";
    case null: "null";
}

Identifier Patterns

Bind the matched value to a variable name. If the identifier is a declared variant constructor, it is a variant pattern instead (see above), not a catch-all:

match x {
    case y: y + 10;  // Binds x to y, then evaluates y + 10
}

Wildcard Pattern

Match anything without binding (use _):

match x {
    case 10: "ten";
    case _: "other";  // Matches anything else
}

Array Patterns

Match arrays by structure and extract elements:

var arr = [1, 2, 3];
match arr {
    case [1, 2, 3]: "exact match";
    case [x, y, z]: "three elements: " + x + ", " + y + ", " + z;
    case [first, ...rest]: "first is " + first + ", rest has " + length(rest) + " elements";
    default: "no match";
}

Rest Pattern: Use ...rest to capture remaining array elements:

var arr = [1, 2, 3, 4, 5];
match arr {
    case [first, second, ...rest]: 
        // first = 1, second = 2, rest = [3, 4, 5]
        first + second + length(rest);
}

Notes:

Object Patterns

Match objects by properties and extract values:

var obj = { name: "Alice", age: 30 };
match obj {
    case { name, age }: name + " is " + age;  // Shorthand: binds name and age
    case { name: userName, age: userAge }: userName + " is " + userAge;  // Explicit binding
    default: "unknown";
}

Nested Object Patterns:

var obj = { user: { name: "Bob", age: 25 }, role: "admin" };
match obj {
    case { user: { name, age }, role }: 
        name + " (" + age + ") is " + role;
}

Notes:

Guards

An optional if condition after the pattern gates the case. The pattern is tried first; bindings from the pattern are in scope for the condition. If the condition is false, the next case is tried. This is the same if word as guarded catch (e if …).

var n = 3;
var result = match n {
    case x if x > 10: "big";
    case x: "small";
};
print(result);

Under --strict-types, a guarded arm does not count as covering a variant or as a catch-all: case Ok(v) if v > 0 does not exhaust Ok, and case x if … is not a default.

Default Case

The default case is optional and matches when no other pattern matches:

match x {
    case 1: "one";
    case 2: "two";
    default: "other";  // Matches anything else
}

If no pattern matches and there's no default case, a runtime error is thrown.

Pattern Matching in Practice

Pattern matching is particularly useful for handling different message types in actors or processing structured data:

// Handle different message types
var msg = receive();
match msg {
    case { type: "Start", payload: p }: handleStart(p);
    case { type: "Stop" }: handleStop();
    case { type: "Update", id: id, value: v }: handleUpdate(id, v);
    default: print("Unknown message type");
}

// Process API responses
var response = fetchData();
match response {
    case { status: 200, data: d }: processData(d);
    case { status: 404 }: print("Not found");
    case { status: 500, error: e }: print("Server error: " + e);
    default: print("Unexpected response");
}
Pattern Matching Tips:

8.4 Exception Handling

Throw Statement

The throw statement throws an exception. The expression can be any value (string, object, etc.).

throw expression;

Example:

throw "Error occurred";
throw "Division by zero";

Try-Catch-Finally

Exception handling allows you to catch and handle errors gracefully.

try {
    // statements that may throw
} catch (exceptionVar if condition) {
    // handle when condition is true (guarded catch)
} catch (exceptionVar) {
    // fallback handler
} finally {
    // cleanup code (always executes)
}

Examples:

// Basic try-catch
try {
    var result = 10 / 0;
} catch (error) {
    print("Error: " + error);
}

// Tagged catch (throw dict with kind/message for e.kind filters)
try {
    throw dict { "kind": "IO", "message": "disk full" };
} catch (e if e.kind == "IO") {
    print("IO: " + e.message);
} catch (e) {
    print("Other: " + e);
}

// For expected success/failure in a return value, prefer result / option
// instead of throw — see Built-in Functions 12.19.1.

// Try-catch-finally
try {
    // risky operation
} catch (error) {
    print("Caught: " + error);
} finally {
    print("Cleanup always runs");
}

// Multiple catch clauses (currently only the first one executes)
try {
    // code
} catch (error) {
    // This catch clause will catch any exception
    // handle error
} catch (otherError) {
    // This catch clause is unreachable - the first one catches all exceptions
    // Note: Exception type matching may be added in future versions
} finally {
    // cleanup
}

Important Notes:

8.5 Deterministic Cleanup

Besides finally, MALDA offers two dedicated forms for releasing resources: defer registers cleanup next to the code that acquired the resource, and the using resource block ties a resource's lifetime to a scope.

defer

A defer block is registered when execution reaches it and runs when the enclosing block or function exits — on normal completion, on return, and on an exception alike:

function process(path) {
    var handle = openHandle(path);
    defer {
        closeHandle(handle);
    }

    // Any exit path below still closes the handle
    if (not isValid(handle)) {
        return null;
    }
    return readAll(handle);
}

Several deferred blocks in the same scope run in reverse registration order (last registered, first executed), which lets you unwind acquisitions in the opposite order to how you made them:

function demo() {
    defer { print("first registered, runs last"); }
    defer { print("second registered, runs first"); }
    print("body");
}

demo();
// body
// second registered, runs first
// first registered, runs last

Rules to keep in mind:

using Resource Blocks

The using name = expression { ... } form binds a resource for the duration of a block and disposes it when the block exits, including on exceptions:

using conn = openConnection(url) {
    var rows = conn.query("SELECT 1");
    print(rows.length);
}
// conn has been disposed here

On exit MALDA looks for a cleanup method on the resource object and calls the first one it finds, in this order:

  1. dispose()
  2. close()
  3. disconnect()

If the resource is null, or is not an object, or defines none of those methods, the block still runs and no cleanup is attempted. The bound name is scoped to the block and is not visible afterwards.

class TempFile {
    public var path;

    function TempFile(path) {
        this.path = path;
    }

    public function close() {
        deleteFile(path);
        print("removed " + path);
    }
}

using tmp = new TempFile("scratch.txt") {
    writeFile(tmp.path, "work in progress");
}
// removed scratch.txt
Choosing between them: use a using block when the resource has a clear disposal method and a clear scope. Use defer when cleanup is not a single method call, when several resources are acquired conditionally, or when the cleanup belongs to a whole function rather than to a nested block. Note that this using is unrelated to the top-level using that aliases a package.

Both forms are supported by the interpreter, the C# transpiler, and the JavaScript transpiler.

See Also