cyrus-lang / cyrus-lang/Cyrus

Interface-Driven Error Handling in Cyrus

Open
#415 0 comments 0 reactions 0 assignees View on GitHub
Feature Request Proposal
Dominant language
Rust
Stars
209
Forks
30
Avg merge
20h 3m
Merged PRs (30d)
25

Description

# Interface-Driven Error Handling in Cyrus

## 1. Motivation

Error handling in systems languages often suffers from two extremes: either highly verbose manual checks (like C's `if (err != 0)`) or "magical" compiler keywords that are hardcoded to specific standard library types (like Zig's error sets or Rust's legacy `try!`).

This proposal introduces an elegant, zero-cost error handling model for Cyrus. By defining a standard compile-time `interface Try`, we unify manual method-based error handling (`unwrap_or`) with ergonomic error propagation (`try`). The compiler does not need hardcoded knowledge of standard types, it solely relies on the interface contract. This means any user-defined data structure can seamlessly hook into the language's native error-handling syntax.

## 2. The Interface Contract

The foundation of this system is the `Try` interface. It defines both the methods used by developers for manual control, and the utility methods required by the compiler to power the `try` keyword.

For example:

```cyrus
// std::errors.cyrus

pub interface Try {
// Manual Handling API
// Used directly by the developer for explicit control
fn unwrap(self: const Self*) T;
fn unwrap_or(self: const Self*, fallback: T) T;

// Compiler-Required API
// Used internally by the compiler to desugar the `try` keyword
fn is_error(self: const Self*) bool;
fn extract_error(self: const Self*) E;
fn extract_value(self: const Self*) T;
}
```

## 3. Standard Library Implementations

Data structures explicitly implement this interface. While interfaces in Cyrus are generally a runtime concept utilized for dynamic dispatch, the `try` keyword treats the `Try` interface as a strict structural blueprint.

Because the compiler knows the exact concrete return type (e.g., `Option` or `ErrorOr`) at the call site, it does not cast the type to a dynamic interface. Instead, it bypasses the runtime vtable entirely and directly calls the concrete implementations of `is_error` and `extract_value`. This guarantees that error propagation via `try` remains a zero-cost, statically dispatched operation, while still strictly enforcing the interface contract.

### Example A: The `Option` Type

For types that represent the presence or absence of a value, the "Error" type is simply `void`.

For example:

```
import std::errors{Try};

pub enum Option : Try {
Some(T),
None,

// Manual API
pub fn unwrap(self: const Self*) T {
switch (self*) {
case .Some(val) => return val;
case .None => @panic("unwrap called on None");
}
}

pub fn unwrap_or(self: const Self*, fallback: T) T {
switch (self*) {
case .Some(val) => return val;
case .None => return fallback;
}
}

// Compiler API
pub fn is_error(self: const Self*) bool { return self* == .None; }
pub fn extract_error(self: const Self*) void { return; }
pub fn extract_value(self: const Self*) T { return self*.Some; }
}
```

### Example B: The `ErrorOr` Type

The standard Result type used for rich error propagation maps cleanly to the interface, preserving the exact error payload.

Code snippet

```
import std::errors{Try};

pub enum ErrorOr : Try {
Value(T),
Err(E),

// Manual API
pub fn unwrap(self: const Self*) T {
switch (self*) {
case .Value(val) => return val;
case .Err(_) => @panic("unwrap called on Err variant");
}
}

pub fn unwrap_or(self: const Self*, fallback: T) T {
switch (self*) {
case .Value(val) => return val;
case .Err(_) => return fallback;
}
}

// Compiler API
pub fn is_error(self: const Self*) bool { return self* == .Err; }
pub fn extract_error(self: const Self*) E { return self*.Err; }
pub fn extract_value(self: const Self*) T { return self*.Ok; }
}
```

## 4. Custom User-Defined Types

Because the `try` keyword relies purely on the interface, developers are not locked into the standard library. If you are writing a custom networking stack, you can create a highly specialized struct and plug it directly into the language's `try` syntax.

Code snippet

```
import std::errors{Try};

// A custom struct for a network pipeline, not an Enum!
pub struct NetworkPacket : Try {
payload: uint8*,
error_code: int32,

pub fn is_error(self: const Self*) bool {
return self*.error_code != 0; // 0 means success
}

pub fn extract_error(self: const Self*) int32 {
return self*.error_code;
}

pub fn extract_value(self: const Self*) uint8* {
return self*.payload;
}

// ...
};
```

## 5. Usage & The `try` Desugaring Process

The beauty of this design lies in its usage. The developer can mix automatic propagation and manual recovery seamlessly.

Code snippet

```
import std::fs{read_file, FileError};
import std::io{StdoutWriter};

pub fn process_config() ErrorOr {
// 1. Automatic Propagation (Powered by Try interface)
// If this fails, it early-returns the FileError automatically.
const raw_data = try read_file("data.txt");

// 2. Manual Fallback
// If the secondary file is missing, we don't abort—we just use a default.
const backup_data = read_file("backup.txt").unwrap_or("default");

return .Value(1);
}
```

### How the Compiler Desugars `try`

When the compiler encounters `try read_file("data.txt")`, it mechanically translates the expression using the guaranteed methods from the `Try` interface. The developer sees the clean `try` keyword, but the compiler generates this explicit control flow:

Code snippet

```
// Compiler's Internal Representation
const _temp = read_file("data.txt");

if (_temp.is_error()) {
// Automatically returns the extracted error up the call stack
return .Err(_temp.extract_error());
}

const raw_data = _temp.extract_value();
```

Contributor guide

No contributing guide indexed for this repository

Research direction

The proposal names std::errors.cyrus and compiler handling of the Try interface and try expression, but no repository implementation files or tests. Start by locating the compiler's interface and expression-lowering entry points, then verify whether the proposed contract fits existing semantics. Done means an agreed design, standard-library implementations, compiler support, and tests for Option, ErrorOr, and custom types.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.