# control flow in ghūl

# block scope

In ghūl, most control flow statements incorporate one or more blocks. A block is a list of one or more statements that forms a scope for local variable definitions. The scope of a variable is the region of code where that variable is visible and can be accessed. Blocks are delimited by keywords that are specific to their control flow statement. For example, if-then statements use then and else, elif or fi to delimit their blocks, while loops use do and od, and so on. Variables defined within a block are only accessible within that block and any nested blocks. Once execution exits the block, those variables go out of scope and cannot be accessed anymore.

# assert statement

In ghūl the assert statement is used to ensure an expected condition holds and to throw an exception if it does not. An assert statement starts with assert, followed by an expression that must evaluate to a bool, followed by else, and then a value to throw. If the value to throw is a string, it will be wrapped in an AssertionFailedException. Otherwise it must be of a throwable type.

assert true else "all bets are off"; // should not throw
assert false else "expect AssertionFailedException";

let list = [1, 2, 3, 4, 5];

assert 3 < list.count else ArgumentOutOfRangeException("list");

# if statement

If statements allow the execution of different code blocks based on specific conditions.

# if-then-fi

This is the simplest form of a conditional statement. It checks a condition and executes the subsequent block of code if the condition is true.

if condition then
    // code to execute if condition is true
fi
let list = [1, 2, 3, 4];

if list.count == 0 then
    write_line("list is empty");
fi

# if-then-else-fi

This form allows for an alternative block of code to be executed if the condition is false.

if condition then
    // code to execute if condition is true
else
    // code to execute if condition is false
fi
if list.count > 0 then
    write_line("list is not empty");
else
    write_line("list is empty");
fi

# if-then-elif-fi

This form is used for multiple conditions. If the initial condition is false, the elif conditions are checked in order. The corresponding block for the first true condition is executed.

if first-condition then
    // code for first condition]
elif second-condition then
    // code for second condition]
... (more elif conditions if needed) ...
else
    // code if all conditions are false
fi
let list = [1, 2, 3, 4];

if list.count == 0 then
    write_line("list is empty");
fi

if list.count > 0 then
    write_line("list is not empty");
else
    write_line("list is empty");
fi

if list.count > 10 then
    write_line("list has lots of elements");
elif list.count > 5 then
    write_line("list has some elements");
elif list.count > 0 then
    write_line("list has a few elements");
else
    write_line("list is empty");
fi

# scope

Each branch of an if statement constitutes a separate scope

let a = 5;

if a > 0 then
    // new scope - neither y nor z are in scope here
    let x = 10
elif a < 0 then
    // new scope - neither x nor z are in scope here
    let y = 20
else
    // new scope - neither x nor y are in scope here
    let z = 30;
fi

# while statement

# while-do-od

The while loop in ghūl executes a block of code repeatedly as long as a specified condition remains true. The condition is evaluated before each iteration of the loop's body.

while condition do
    // code to execute while the condition is true
od
let counter = 0;
while counter < 5 do
    write_line(counter);
    counter = counter + 1;
od

This loop prints numbers from 0 to 4. It terminates when counter becomes 5, as the condition counter < 5 then evaluates to false.

# break and continue in while loops

The break statement immediately exits the loop, while continue skips the remaining code in the current iteration and proceeds to the next iteration immediately before the condition is reevaluated.

let counter = 0;
while counter < 10 do
    if counter == 5 then
        break;
    fi
    write_line(counter);
    counter = counter + 1;
od

This loop exits when counter reaches 5 without proceeding to execute `write_line(counter)``

let counter = 0;
while counter < 5 do
    counter = counter + 1;
    if counter == 3 then
        continue;
    fi
    write_line(counter);
od

This loop skips the call to write_line when counter is 3.

# scope

The block statement body of the while statement, delimited by do and od forms a scope for local variable definitions.

# for statement

# for-in-do-od

The for loop in ghūl steps through an iterable object executing the loop body once for every value the iterator produces. An iterable object is something that implements either Collections.Iterable[T] or Collections.Iterator[T], and the loop variable's type is inferred to be T.

for variable in iterable do
    // variable is set to each element of iterator in turn
od

The variable is defined by the for loop and its scope is the for loop body from the do up to the od

// i not in scope here
for i in [1, 2, 3, 4, 5] do // i defined here, with type `int`
    // i in scope here:
    write_line(i);
od

# range operators

The .. and :: operators construct integer ranges that can be iterated over by for statements. .. constructs ranges that are inclusive of its left operand and exclusive of its right operand:

for i in 0..5 do
    // i will take values 0, 1, 2, 3, 4 in sequence
    write_line(i);
od

:: constructs a range that is inclusive of its left and right operands:

for i in 1::5 do
    // i will take values 1, 2, 3, 4, 5 in sequence
    write_line(i);
od

These operators are not for loop specific and can be used in any expression context

let zero_to_four = 0..5;
let five_to_nine = 5..10;

let zero_to_nine = zero_to_four | .cat(five_to_nine);

while zero_to_nine.has_next() do
    write_line(range.next())
od

# break and continue in for loops

The break statement immediately exits the loop, while continue skips the remaining code in the current iteration and proceeds to the next iteration immediately before attempting to read the next element from the iterator

for counter in 0..10 do
    if counter == 5 then
        break;
    fi
    write_line(counter);
od

This loop exits when counter reaches 5, without proceeding to execute write_line(5)

for counter in 0..5 do
    counter = counter + 1;
    if counter == 3 then
        continue;
    fi
    write_line(counter);
od

This loop skips the call to write_line when counter is 3.

# scope

The block statement body of the for statement, delimited by do and od forms a scope for local variable definitions. The loop variable is in scope within this block scope but not within the expression that provides the iterable object.

# do statement

# do-od

The do / od loop in ghūl is used to create an indefinite loop which will continue to execute until explicitly broken with a break statement.

do
    // code to execute indefinitely
    // break statement to exit loop
od
let counter = 0;
do
    write_line(counter);
    counter = counter + 1;
    if counter == 5 then
        break;
    fi
od

This loop will run indefinitely until counter reaches 5, at which point the break statement terminates the loop.

# break and continue in do-od loops

The break and continue statements work similarly in do / od loops as they do in while loops.

let counter = 0;
do
    counter = counter + 1;
    if counter == 3 then
        continue;
    fi
    write_line(counter);
    if counter == 5 then
        break;
    fi
od

This loop skips the write_line statement when counter is 3 and breaks out of the loop when counter reaches 5.

# scope

The block statement body of the do statement, delimited by do and od forms a scope for local variable definitions.

# case statement

case value
when -1:
    return "minus one";

when 0:
    let result = "zero";
    return result;

when 1:
    return "one";

when 2:
    return "two";

when 3:
    return "three";

when 4:
    return "four";

when 5:
    let result = "five";
    return result;

when 6, 7, 8, 9:
    return "more than five and less than ten";

when 13:
    return "unlucky";

default
    return "less than -1 or more than nine";
esac

# scope

Each arm of the case statement, delimited by either a when clause or default forms a separate scope for local variable definitions.

# try statement

# try-catch-finally-yrt

The try-catch-finally-yrt block in ghūl consists of four parts:

  • try block: the block where code that might throw an exception is placed.
  • exception to catch: exceptions that are assignment compatible with this class will be caught and control will enter the catch block
  • catch block: this code block catches and handles exceptions. It takes an exception variable and a type.
  • finally block: this code block is executed after the try and catch blocks, regardless of whether an exception was thrown or not. It is typically used for clean-up code.
try
    // Code that might throw an exception
catch e: SomeExceptionType
    // Exception handling code
finally
    // Clean-up code, always executed
yrt

If different types of exception should be caught, then there can be multiple exception clauses and catch blocks

let reader: StreamReader;

try
    reader = StreamReader("file.txt");
    let content = reader.read_to_end();

    write_line(content);

catch e: FileNotFoundException
    // Handle the case where the file is not found
    write_line("Error: file not found: " + e.message);
catch e: IOException
    // Handle errors during file reading
    write_line("Error: reading file: " + e.message);
finally
    // Close the file and clean up resources
    if reader? then
        reader.close();
    fi

    write_line("File processing completed, file closed.");
yrt

# try-catch-yrt

The finally clause can be omitted if no clean-up is required

try
    // Code that might throw an exception
catch e: SomeExceptionType
    // Exception handling code
yrt
try
    let content = File.read_all_text("file.txt");
    write_line(content);

    write_line("File processing completed.");
catch e: FileNotFoundException
    // Handle the case where the file is not found
    write_line("Error: file not found: " + e.message);
catch e: IOException
    // Handle errors during file reading
    write_line("Error: reading file: " + e.message);
yrt

# try-finally-yrt

The catch clause can be omitted if no exceptions need to be caught but clean-up is still required

try
    // Code that might throw an exception
finally
    // Clean-up code, always executed
yrt
let reader: StreamReader;

try
    reader = StreamReader("file.txt");

    let content = reader.read_to_end();
    write_line(content);

    write_line("File processing completed.");

finally
    if reader? then
        reader.close();
    fi

    // Any exceptions will be thrown to the calling code
yrt

# return statement

# return without value

In functions of void return type, a bare return statement with no value returns control flow directly to the caller

let tries = 0;

...

try_something(limit: int) is
    if tries > limit then
        return; // give up
    fi

    tries = tries + 1;

    // do stuff
si

# return value

In functions of non-void return type, return statements must return a value of a type that's assignment compatible with the function's return type

fib(n: int) -> int is
    if n < 0 then
        return 0;
    elif n == 1 then
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
    fi
si

# default return

If execution reaches the end of a non-void function without encountering a return statement, then the default value of the function's return type is returned to the caller.

default_return() -> int is
    // do nothing, return 0
si

...

let i = default_return();
assert i == 0;