Interface-Driven Error Handling in Cyrus
- 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