Compiler Patterns

Ori provides special patterns that the compiler handles with optimized code generation. These patterns provide powerful abstractions with zero overhead.

Pattern Categories

Patterns fall into two categories:

Block expressions — Sequential expressions (order matters):

  • { } blocks — sequential evaluation with bindings
  • try { } — error propagation
  • match expr { } — pattern matching

function_exp — Named expressions:

  • recurse — self-referential recursion
  • cache — cached computation
  • with — resource management
  • catch — panic capture

Block Expressions

Sequential expressions where each step can use previous results:

{
    let a = compute_a();
    let b = compute_b(input: a);
    let c = compute_c(x: a, y: b);

    c
}

Basic Usage

@process_user (id: int) -> UserProfile = {
    let user = fetch_user(id: id);
    let orders = fetch_orders(user_id: user.id);
    let stats = calculate_stats(orders: orders);
    UserProfile { user, orders, stats }
}

Scope and Bindings

Each binding is available to subsequent expressions:

{
    let x = 10;
    let y = x * 2;       // Can use x
    let z = x + y;       // Can use x and y
    print(msg: `{x} {y} {z}`);

    z                   // Final value is z
}

Side Effects

Blocks are for sequential operations with side effects:

@save_and_notify (user: User) -> void = {
    save_to_database(user: user);
    send_email(to: user.email, subject: "Welcome!");
    log_event(type: "user_created", data: user.id)
}

Function Contracts

Add preconditions and postconditions on the function declaration, between the return type and the =:

@sqrt (x: float) -> float
    pre(x >= 0.0 | "x must be non-negative")
    post(result -> result >= 0.0)
= compute_sqrt(x: x)
  • pre(...) — verified before the body runs
  • post(...) — verified after, receives the result as parameter
  • | "message" — custom error message (panics with this message if check fails)

Contract Examples

@divide (a: int, b: int) -> int
    pre(b != 0 | "division by zero")
= a / b

@clamp (value: int, min: int, max: int) -> int
    pre(min <= max | "min must not exceed max")
    post(result -> result >= min && result <= max)
= if value < min then min else if value > max then max else value

@factorial (n: int) -> int
    pre(n >= 0 | "factorial undefined for negative numbers")
    post(result -> result > 0 | "factorial must be positive")
= if n <= 1 then 1 else n * factorial(n: n - 1)

The try Pattern

A block designed for error propagation:

try {
    let a = fallible_a()?;
    let b = fallible_b(input: a)?;
    let c = fallible_c(x: a, y: b)?;
    Ok(c)
}

The ? operator:

  • Extracts Ok(v)v
  • Propagates Err(e) → returns early with Err(e)

Error Traces

try automatically collects error traces:

@load_config () -> Result<Config, Error> = try {
    let data = read_file(path: "config.json")?;   // Trace point
    let config = parse_json(data: data)?;          // Trace point

    Ok(config)
}

If parsing fails, the trace shows:

Error: invalid JSON
Trace:
  at load_config (config.ori:3:18)

Mixing try and blocks

Use try { } for fallible code, plain { } for infallible:

@process_batch (items: [int]) -> Result<Summary, Error> = try {
    let results = for item in items yield process_item(id: item)?;

    // Plain block for non-fallible computation
    let summary = {
        let total = len(collection: results);
        let sum = results.iter().fold(initial: 0, op: (a, b) -> a + b);
        Summary { total, average: sum / total }
    };

    Ok(summary)
}

The match Pattern

Pattern matching with exhaustiveness checking:

match value {
    Pattern1 -> result1
    Pattern2 -> result2
    _ -> default
}

Match Must Return Values

Match is an expression — all arms must return the same type:

let description = match status {
    Active -> "Currently active"
    Inactive -> "Not active"
    Pending -> "Waiting for approval"
};

Exhaustiveness

The compiler ensures all cases are covered:

type Color = Red | Green | Blue;

// ERROR: non-exhaustive match
let name = match color {
    Red -> "red"
    Green -> "green"
    // Missing Blue!
};

// OK: all cases covered
let name = match color {
    Red -> "red"
    Green -> "green"
    Blue -> "blue"
};

The recurse Pattern

Self-referential recursion with optional memoization:

@fibonacci (n: int) -> int = recurse(
    condition: n <= 1,
    base: n,
    step: self(n: n - 1) + self(n: n - 2),
    memo: true,
)

Parameters

ParameterPurpose
conditionWhen to return base case
baseValue for base case
stepRecursive computation (use self())
memoEnable memoization (default: false)
parallelParallelize for n > threshold (optional)

Without Memoization

@factorial (n: int) -> int = recurse(
    condition: n <= 1,
    base: 1,
    step: n * self(n: n - 1),
)

With Memoization

@fibonacci (n: int) -> int = recurse(
    condition: n <= 1,
    base: n,
    step: self(n: n - 1) + self(n: n - 2),
    memo: true,
)

With memo: true, results are cached — the second call to fibonacci(n: 10) is instant.

With Parallelization

@parallel_fib (n: int) -> int = recurse(
    condition: n <= 1,
    base: n,
    step: self(n: n - 1) + self(n: n - 2),
    memo: true,
    parallel: 20,  // Parallelize for n > 20
)

How self() Works

self() is a special reference to the enclosing recursive function:

@tree_depth<T> (node: TreeNode<T>) -> int = recurse(
    condition: is_leaf(node: node),
    base: 0,
    step: 1 + max(
        left: self(node: node.left),
        right: self(node: node.right),
    ),
)

The cache Pattern

Cache expensive computations:

@get_user (id: int) -> Result<User, Error> uses Http, Cache =
    cache(
        key: `user:{id}`,
        op: Http.get(url: `/users/{id}`),
        ttl: 5m,
    )

Parameters

ParameterPurpose
keyCache key (string)
opExpression to compute if not cached
ttlTime-to-live for cached value

Cache Behavior

  1. Check if key exists in cache
  2. If exists and not expired, return cached value
  3. If not exists or expired, evaluate op
  4. Store result with ttl
  5. Return result

Requires Cache Capability

@get_user_cached (id: int) -> Result<User, Error> uses Http, Cache =
    cache(
        key: `user:{id}`,
        op: Http.get(url: `/users/{id}`),
        ttl: 5m,
    )

// Test with mock cache
@test_cache tests @get_user_cached () -> void =
    with Http = MockHttp { responses: { "/users/1": `{"id": 1}` } },
         Cache = MockCache {} in {
        let first = get_user_cached(id: 1);   // Fetches from Http
        let second = get_user_cached(id: 1);  // Returns from cache
        assert_ok(result: first);
        assert_ok(result: second)
    }

The with Pattern

Resource management with guaranteed cleanup:

@process_file (path: str) -> Result<str, Error> uses FileSystem =
    with(
        acquire: FileSystem.open(path: path),
        use: file -> FileSystem.read_all(file: file),
        release: file -> FileSystem.close(file: file),
    )

Parameters

ParameterPurpose
acquireExpression to acquire resource
useFunction to use resource
releaseFunction to release resource (always runs)

Guaranteed Cleanup

release always runs, even if use fails:

@safe_transaction (db: Database) -> Result<void, Error> uses Database =
    with(
        acquire: db.begin_transaction(),
        use: tx -> {
            tx.insert(table: "users", data: user_data)
            tx.update(table: "stats", data: stats_data)
            Ok(())
        },
        release: tx -> tx.rollback_if_uncommitted(),
    )

Similar to try-finally

The with pattern is similar to try-finally or RAII:

# Python equivalent
try:
    resource = acquire()
    return use(resource)
finally:
    release(resource)

The catch Pattern

Capture panics as Results:

let result = catch(expr: might_panic());
// Result<T, str>

match result {
    Ok(value) -> print(msg: `Got: {value}`)
    Err(msg) -> print(msg: `Panic caught: {msg}`)
};

When to Use catch

Use sparingly — panics indicate bugs, not expected errors:

// Good: Test frameworks
@test_panics tests @divide () -> void = {
    let result = catch(expr: divide(a: 1, b: 0));
    assert_err(result: result)
}

// Good: Plugin systems
@run_plugin (plugin: Plugin) -> Result<void, str> =
    catch(expr: plugin.execute());

// Good: REPL environments
@eval_safely (code: str) -> Result<Value, str> =
    catch(expr: evaluate(code: code));

catch vs Result

ApproachUse Case
ResultExpected, recoverable errors
catchIsolating untrusted code, test frameworks

The for Pattern (Advanced)

The for pattern has an advanced form:

for(
    over: items,
    match: pattern,
    default: fallback,
)

With Pattern Matching

@extract_names (data: [Option<User>]) -> [str] =
    for(
        over: data,
        match: Some(user) -> user.name,
        default: continue,
    )

With Map Function

@process_all (items: [int]) -> [int] =
    for(
        over: items,
        map: x -> x * 2,
    )

Combining Patterns

Contracts with recurse

@tree_sum<T: Addable> (node: TreeNode<T>) -> T
    pre(!is_null(node: node))
= recurse(
    condition: is_leaf(node: node),
    base: node.value,
    step: node.value + self(node: node.left) + self(node: node.right),
)

try with cache

@fetch_cached (url: str) -> Result<str, Error> uses Http, Cache = try {
    let data = cache(
        key: `fetch:{url}`
        op: Http.get(url: url)?
        ttl: 10m
    );
    Ok(data)
}

with and try

@safe_file_op (path: str) -> Result<Data, Error> uses FileSystem = try {
    let result = with(
        acquire: FileSystem.open(path: path)?
        use: file -> parse_data(content: FileSystem.read(file: file)?)
        release: file -> FileSystem.close(file: file)
    );
    Ok(result)
}

Complete Example

type Config = {
    database_url: str,
    cache_ttl: Duration,
    max_retries: int,
}

type User = { id: int, name: str, email: str }

// Load config with validation
@load_config (path: str) -> Result<Config, Error> uses FileSystem = try {
    let content = FileSystem.read(path: path)?;
    let config = parse_config(data: content)?;
    if config.max_retries <= 0 then panic(msg: "max_retries must be positive");
    if config.cache_ttl <= 0s then panic(msg: "cache_ttl must be positive");

    Ok(config)
}

@test_load_config tests @load_config () -> void =
    with FileSystem = MockFileSystem {
        files: {
            "config.json": `{"database_url": "...", "cache_ttl": "5m", "max_retries": 3}`,
        },
    } in {
        let result = load_config(path: "config.json");
        assert_ok(result: result)
    }

// Fetch user with caching
@get_user (id: int) -> Result<User, Error> uses Http, Cache =
    cache(
        key: `user:{id}`,
        op: Http.get(url: `/api/users/{id}`),
        ttl: 5m,
    )

// Recursive data processing
@flatten_tree<T> (node: TreeNode<T>) -> [T] = recurse(
    condition: is_leaf(node: node),
    base: [node.value],
    step: [node.value, ...self(node: node.left), ...self(node: node.right)],
)

// Resource-safe database operation
@with_connection<T> (
    url: str,
    op: (Connection) -> Result<T, Error>,
) -> Result<T, Error> uses Database =
    with(
        acquire: Database.connect(url: url),
        use: conn -> op(conn),
        release: conn -> conn.close(),
    )

// Combining multiple patterns
@process_users (ids: [int]) -> Result<[User], Error>
    uses Http, Cache, Logger, Async = try {
    let users = parallel(
        tasks: for id in ids yield () -> {
            Logger.debug(msg: `Fetching user {id}`);
            get_user(id: id)
        }
        max_concurrent: 10
        timeout: 30s
    );

    // Extract successful results
    let valid_users = for result in users
        if is_ok(result: result)
        yield match result {
            Ok(user) -> user
            Err(_) -> continue
        };

    Ok(valid_users)
}

@test_process_users tests @process_users () -> void =
    with Http = MockHttp {
        responses: {
            "/api/users/1": `{"id": 1, "name": "Alice", "email": "a@test.com"}`,
            "/api/users/2": `{"id": 2, "name": "Bob", "email": "b@test.com"}`,
        },
    },
    Cache = MockCache {},
    Logger = MockLogger {} in {
        let result = process_users(ids: [1, 2]);
        assert_ok(result: result);
        match result {
            Ok(users) -> assert_eq(actual: len(collection: users), expected: 2)
            Err(_) -> panic(msg: "Expected Ok")
        }
    }

Quick Reference

Blocks

{
    let a = ...;
    let b = ...;

    result
}

// Contracts go on the function declaration:
@name (params) -> ReturnType
    pre(condition | "error message")
    post(result -> condition)
= body

try

try {
    let a = fallible()?;
    let b = fallible()?;
    Ok(result)
}

match

match value {
    Pattern1 -> result1
    Pattern2 -> result2
    _ -> default
}

recurse

recurse(
    condition: base_case_condition,
    base: base_case_value,
    step: self(...) + self(...),
    memo: true,
    parallel: threshold,
)

cache

cache(
    key: "cache_key",
    op: expensive_computation,
    ttl: 5m,
)

with

with(
    acquire: get_resource,
    use: resource -> use_resource(r: resource),
    release: resource -> cleanup(r: resource),
)

catch

catch(expr: might_panic()) -> Result<T, str>

What’s Next

Now that you understand compiler patterns: