1. Catching exceptions

The main idea of exception is the possibility to recover from a failing program state. In order to do that, another scope guard exits in Ymir, this scope guard is named catch. The syntax of this scope guard is relatively close to the other scope guard, and to the pattern matching. Indeed, this scope guard does not execute an expression but match over an exception that has been caught. The following code block present the grammar of the catch scope guard. The pattern_expression used in this code block are those defined in the chapter Pattern matching.

guarded_scope := '{' expression ((';')? expression)* (';')? '}' guards
guards := (Guard expression)* ('catch' catching_expression)? (Guard expression)*
Guard := 'exit' | 'success' | 'failure'

catching_expression := pattern_var | pattern_call | '_'

pattern_var := (Identifier | '_') ':' (type | '_') ('=' pattern_expression)?
pattern_call := (type | '_') '(' (pattern_argument (',' pattern_argument)*)? ')'
pattern_arguments := (Identifier '->')? pattern_expression

1.1. Catch everything

Catch scope guard can be used to catch any exception and continue the execution of the program. In the following example, the main function calls the foo function, that throws an &AssertError. The call is guarded by a catch expression, that catch every kind of Exception (using the _ token). Because the exception of the foo function is caught the main function is considered safe, and thus cannot throw an exception, this is why nothing is declared in its prototype. In this example, the program ends normaly, after exiting the main function.

import std::io;

def foo (i : i32) 
    throws &AssertError
{
    assert (i < 10, "i must be lower than 10");
    println (i);
}

def main () {
    println ("Before foo");
    { 
        foo (10); 
    } catch {
        _ => {
            println ("Foo failed");
        }
    }
    println ("After foo");
}


Results:

Before foo
Foo failed
After foo


A variable pattern can also be used to get the value of the exception. There is no much change in the following example, in comparison to the previous one, except that the main function prints the exception that has been throw by the foo function. In debug mode (-g option of the compiler), when throwing an exception, the stack trace is accessible and printed, when printing an exception. This stack trace (for efficiency reasons) is not created in release mode.

import std::io;

def foo (i : i32) 
    throws &AssertError
{
    assert (i < 10, "i must be lower than 10");
    println (i);
}

def main () {
    println ("Before foo");
    { 
        foo (10); 
    } catch {
        err : _ => {
            println ("Foo failed : ", err);
        }
    }
    println ("After foo");
}


Results:

Before foo
Foo failed : core::exception::AssertError (i must be lower than 10):
╭  Stack trace :
╞═ bt ╕ #1 in function core::exception::AssertError::self (...)
│     ╘═> /home/emile/ymir/Runtime/midgard/core/exception.yr:49
╞═ bt ╕ #2 in function core::exception::abort (...)
│     ╘═> /home/emile/ymir/Runtime/midgard/core/exception.yr:84
╞═ bt ╕ #3 in function main::foo (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:7
╞═ bt ╕ #4 in function main (...)
│     ╘═> /home/emile/Documents/test/ymir/main.yr:14
╞═ bt ╕ #5
│     ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #6 in function main
│     ╘═> /home/emile/Documents/test/ymir/main.yr:10
╞═ bt ╕ #7
│     ╘═> /lib/x86_64-linux-gnu/libc.so.6:??
╞═ bt ═ #8 in function _start
╰
After foo

1.2. Catch a specific exception

The content of a catch scope guard is a list of patterns, that can be used to get a different behavior for different kind of exception. In the following example, the foo function, can throw two different kind of exception, &AssertError and &OutOfArray. The catch of the main function has a different behavior if the exception that is thrown is a &AssertError or a &OutOfArray, (by using a variable pattern for &AssertError, and a call pattern for &OutOfArray).

import std::io;

def foo (i : [i32]) 
    throws &AssertError, &OutOfArray
{
    assert (i [0] < 10, "i[0] must be lower than 10");
    println (i);
}

def main () {
    println ("Before foo");
    { 
        foo ([]); 
    } catch {
        err : &AssertError => {
            println ("Foo failed : ", err);
        }
        OutOfArray () => {
            println ("Foo failed, the slice was empty");
        }
    }
    println ("After foo");
}


Results:

Before foo
Foo failed, the slice was empty
After foo


1.3. Rethrowing exceptions

Catch scope guard must catch every exceptions that are thrown by the scope that is guarded. For example, if the main function of the previous example, was defined as presented in the next code block, the compiler would have returned an error. Indeed, in this example, the &OutOfArray exception is not managed by the catch guard.

def main () {
    println ("Before foo");
    { 
        foo ([]); 
    } catch {
        err : &AssertError => {
            println ("Foo failed : ", err);
        }
    }
    println ("After foo");
}


Errors:

Error : the exception &(core::array::OutOfArray) might be thrown but is not caught
 --> main.yr:(13,7)
13  ┃         foo ([]); 
    ╋             ^
    ┃ Note : 
    ┃  --> main.yr:(14,4)
    ┃ 14  ┃     } catch {
    ┃     ╋       ^^^^^
    ┗━━━━━┻━ 


ymir1: fatal error: 
compilation terminated.


The OutOfArray exception can be rethrown inside the catch scope guard to remove this error. In that case the stack trace is still correct, as it is created by the constructor of the Exception class.

def main () 
    throws &OutOfArray
{
    println ("Before foo");
    { 
        foo ([]); 
    } catch {
        err : &AssertError => {
            println ("Foo failed : ", err);
        }
        x : &OutOfArray => throw x;
    }
    println ("After foo");
}

1.4. Catch as a value

In some cases (many cases), a scope is used to compute a value. In that circumstance, if an error occurs inside the scope, the value of the scope is not set, and cannot be used. For example, in the following source code, the main function tries to initialize a variable from the value of the foo function. This function is not safe, and might return no value at all. In order to guarantee, that the variable x is initialized, and usable no matter what happened in the foo function, the catch scope guard can be used as the default value. This is presented as well at line 21 to initialize the variable y.

import std::io;

def foo (i : i32) 
    throws &AssertError
{
    assert (i < 10, "i[0] must be lower than 10");
    i + 12
}

def main ()
{
    {
        let x = foo (10); 
        println (x);
    } catch {
        _ => {
            println ("Foo failed");
        }
    }

    let y = {
        foo (10)
    } catch {
        _ => {
            42
        }
    }

    println (y);
}


Results:

Foo failed
42


Because, the catch scope guard is a pattern matching, multiple branch can be entered. In that case, every branch of the catch guard must share the same type. In the following example, the value y is set conditionaly, depending on the type of exception that is thrown by the foo function, or if the function foo succeeds. In this example, the foo function throws a &OutOfArray exception, thus the variable y is set to 42 (value computed at line 19).

import std::io;

def foo (i : [i32]) -> i32
    throws &AssertError, &OutOfArray
{
    assert (i[0] < 10, "i[0] must be lower than 10");
    i[0] + 12
}

def main ()
{
    let y = {
        foo ([])
    } catch {
        AssertError () => {
            11
        }
        OutOfArray () => {
            42
        }
    }

    println (y);
}


Results:

42

1.5. Catch along other scope guards

Catch scope guards can be used along other scope guards, (success, failure and exit), but there can be only one catch scope guard per scope . In that case the priority is the following: 1) failure, 2) catch, 3) exit. If there is a success scope guard that is executed, then the catch scope guard cannot be executed, so the priority over these two guards is not defined.

import std::io;

def foo (i : [i32]) 
    throws &AssertError, &OutOfArray
{
    assert (i [0] < 10, "i[0] must be lower than 10");
    println (i);
}

def main ()
{
    println ("Before foo");
    { 
        foo ([]); 
    } catch {
        err : &AssertError => {
            println ("Foo failed : ", err);
        }
        _ : &OutOfArray => {
            println ("Out");
        }
    } success {
        println ("Succ");
    } failure {
        println ("Fails");
    } exit {
        println ("Exit");
    }
    println ("After foo");
}


Results:

Before foo
Fails
Out
Exit
After foo


The behavior is exactly the same when catch scope guard has a value.

import std::io;

def foo (i : [i32]) -> i32
    throws &AssertError, &OutOfArray
{
    assert (i[0] < 10, "i[0] must be lower than 10");
    i[0] + 12
}

def main ()
{
    let y = {
        foo ([])
    } catch {
        AssertError () => {
            println ("Assert");
            11
        }
        OutOfArray () => {
            println ("Out");
            42
        }
    } failure {
        println ("Fails");
    } exit {
        println ("Exit");
    }

    println (y);
}


Results:

Fails
Out
Exit
42

results matching ""

    No results matching ""