Aranya Documentation An overview of the Aranya project

The “never” type and control flow escape

Some expressions in the policy language don’t actually produce values, because they cause some kind of escape from their evaluation. todo(), for example, immediately panics the VM with an error. or return within a function, which exits the function with a value. These expressions internally have the “never” type, which means they will never produce a value.

Practically, that means these expressions can work as any type because their value will never be calculated. Which means they can exist in some weird places.

function foo() bool {
    let x = query Foo[x: return false]
}

I recommend you don’t do this.

or operator

or is an optional coalescing operator that combines unwrap with a fallback value, similar to Rust’s .unwrap_or() or JavaScript’s nullish coalescing operator ??.

fact Capability[object_id id] => {
    read bool,
    write bool,
}

// default `struct Capability` created by fact above
function default_capability(object_id id) struct Capability {
    return Capability {
        object_id: object_id,
        read: false,
        write: false,
    }
}

function get_capability(object_id id) struct Capability {
    let cap_opt = query Capability[object_id: object_id]
    return cap_opt or default_capability(object_id)
}

There are two subexpressions on either side of or. The left subexpression must be an optional type. If the left subexpression evaluates to Some(v), it is unwrapped and v is the value of the expression. If it is None, then the value of the expression is the value of the right subexpression. let x = a or b is equivalent to:

let x = if a is Some {
    unwrap a
} else {
    b
}

And just like the if arms, the inner type of the left-hand optional must match the type of the right-hand subexpression. or always evaluates to a single type. So it cannot, for example, convert None into Err() (unless the optional’s inner type is an appropriate result).

or lazily evaluates its right side expression, performing the evaluation only when the left side is None. So or can be naturally chained across several optional expressions until one is Some, followed by a default value, return, or recall.

let authorized_user = primary_user() or admin() or return Err("user not found")

or is valid in any context.

recall

Recall is both an expression and statement that immediately transfers policy evaluation to the named recall block.

command Foo {
    ...

    policy {
        let user = get_user(this.user_id) or recall invalid_user(this.user_id)

        if user.privilege_level < ADMIN_LEVEL {
            recall insufficient_privileges(user)
        }
    }

    recall invalid_user(user_id id) {
        // handle the error
    }

    recall insufficient_privileges(user struct User) {
        // ...
    }
}

The recall keyword is followed by a function call-like item that refers to a recall block within the command. The command is recalled, and evaluation continues at the named recall block.

In expression form, recall has the “never” type, as it does not produce a value and instead transfers control.

recall is valid only within policy blocks.

check ... else ...

The expression in check is now followed by an else keyword and a terminal expression. Specifically, that means something that has the “never” type.

check authorized else recall unauthorized()
check valid_user(user_id) else return Err(Error::InvalidUser)

Thus, failing the checked expression still performs an early exit, but check is now a more explicit and flexible control flow statement.

check is still valid in policy blocks and functions, but the use of either recall or return is restricted to policy blocks and functions, respectively.

Removal of unwrap and check_unwrap

The or statement should provide a safe alternative to unwrap and check_unwrap, so they are removed from the language.

Migrating error handling

Unwrap panicking is still possible by explicitly invoking todo(), which makes it more clear that the error is not being handled.

// let x = unwrap y
let x = y or todo()

But what is better is using or to report error conditions by early exiting with a recall (within policy blocks) or returning a result (within pure functions).

let x = y or recall policy_failed()
let x = y or return Err(Error::Y)

A common use case for check_unwrap was to produce a check failure on a nonexistent fact. This now looks more clear by use of or with recall.

// let data = check_unwrap query Foo[user_id: user_id]
let data = query Foo[user_id: user_id] or recall user_not_found()